113 lines
3.7 KiB
Vue
113 lines
3.7 KiB
Vue
<script setup>
|
|
const props = defineProps({
|
|
url: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
description: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
thumbnail: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 북마크 카드에 표시할 호스트명을 반환한다.
|
|
* @returns {string} www 없는 호스트 또는 빈 문자열
|
|
*/
|
|
const displayHost = computed(() => {
|
|
try {
|
|
return new URL(props.url).hostname.replace(/^www\./, '')
|
|
} catch {
|
|
return ''
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 썸네일이 비었을 때 파비콘 보조 URL을 만든다.
|
|
* @returns {string} favicon 요청 URL
|
|
*/
|
|
const faviconUrl = computed(() => {
|
|
if (!displayHost.value) {
|
|
return ''
|
|
}
|
|
|
|
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(displayHost.value)}&sz=128`
|
|
})
|
|
|
|
/**
|
|
* 실제로 표시할 이미지 주소
|
|
* @returns {string}
|
|
*/
|
|
const imageSrc = computed(() => props.thumbnail || faviconUrl.value)
|
|
|
|
/**
|
|
* 표시 제목(없으면 호스트·URL)
|
|
* @returns {string}
|
|
*/
|
|
const displayTitle = computed(() => props.title || displayHost.value || props.url)
|
|
|
|
/**
|
|
* 외부 링크로 열어도 되는 URL인지 확인한다.
|
|
* @returns {boolean} 허용 여부
|
|
*/
|
|
const isSafeBookmarkUrl = computed(() => {
|
|
try {
|
|
const parsedUrl = new URL(props.url)
|
|
return ['http:', 'https:'].includes(parsedUrl.protocol)
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<a
|
|
v-if="isSafeBookmarkUrl"
|
|
class="prose-bookmark group prose-bookmark-card my-8 flex max-w-full flex-col overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] no-underline transition-[background-color,box-shadow] hover:bg-[color-mix(in_srgb,var(--site-panel)_86%,var(--site-text)_14%)] sm:flex-row"
|
|
:href="url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<div class="prose-bookmark__media relative h-36 w-full shrink-0 overflow-hidden bg-[color-mix(in_srgb,var(--site-line)_40%,var(--site-panel))] sm:h-auto sm:w-[min(44%,220px)] sm:min-h-[9rem]">
|
|
<img
|
|
v-if="imageSrc"
|
|
class="prose-bookmark__thumb h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
|
:src="imageSrc"
|
|
alt=""
|
|
loading="lazy"
|
|
>
|
|
</div>
|
|
<div class="prose-bookmark__body flex min-w-0 flex-1 flex-col justify-center gap-1 px-4 py-4 sm:px-5 sm:py-5">
|
|
<p v-if="displayHost" class="prose-bookmark__host text-[11px] font-semibold uppercase tracking-[0.06em] text-[var(--site-muted)]">
|
|
{{ displayHost }}
|
|
</p>
|
|
<p class="prose-bookmark__title text-[15px] font-semibold leading-snug text-[var(--site-text)]">
|
|
{{ displayTitle }}
|
|
</p>
|
|
<p v-if="description" class="prose-bookmark__desc line-clamp-2 text-sm leading-relaxed text-[var(--site-muted)]">
|
|
{{ description }}
|
|
</p>
|
|
<p class="prose-bookmark__meta mt-1 flex items-center gap-1.5 text-xs font-medium text-[var(--site-soft)]">
|
|
<svg class="shrink-0 opacity-80" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
<path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6" />
|
|
<path d="M11 13l9 -9" />
|
|
<path d="M15 4h5v5" />
|
|
</svg>
|
|
<span class="truncate">{{ url }}</span>
|
|
</p>
|
|
</div>
|
|
</a>
|
|
<p v-else class="prose-bookmark prose-bookmark-invalid my-8 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] p-5 text-sm font-semibold text-[var(--site-muted)]">
|
|
지원하지 않는 북마크 URL입니다.
|
|
</p>
|
|
</template>
|