Continue reading @@ -60,7 +115,7 @@ .link-preview { background: $grey-97; border: 1px solid $grey-90; - border-radius: $unit; + border-radius: $card-corner-radius; padding: $unit-2x; margin-bottom: $unit-3x; @@ -119,4 +174,116 @@ font-weight: 500; transition: all 0.2s ease; } + + // Embed preview styles + .embed-preview { + margin: $unit-2x 0; + } + + .youtube-embed-preview { + .youtube-player { + position: relative; + width: 100%; + padding-bottom: 56%; // 16:9 aspect ratio + height: 0; + overflow: hidden; + background: $grey-95; + border-radius: $card-corner-radius; + border: 1px solid $grey-85; + + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; + border-radius: $unit; + } + } + } + + .url-embed-preview { + display: flex; + flex-direction: column; + background: $grey-97; + border-radius: $card-corner-radius; + overflow: hidden; + border: 1px solid $grey-80; + text-decoration: none; + transition: all 0.2s ease; + width: 100%; + + &:hover { + border-color: $grey-80; + transform: translateY(-1px); + box-shadow: 0 0 8px rgba(0, 0, 0, 0.08); + } + + .embed-image { + width: 100%; + aspect-ratio: 2 / 1; + overflow: hidden; + background: $grey-90; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .embed-text { + flex: 1; + padding: $unit-2x $unit-3x $unit-3x; + display: flex; + flex-direction: column; + gap: $unit; + min-width: 0; + } + + .embed-meta { + display: flex; + align-items: center; + gap: $unit-half; + font-size: 0.8125rem; + color: $grey-40; + } + + .embed-favicon { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + .embed-domain { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-transform: lowercase; + } + + .embed-title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: $grey-10; + line-height: 1.3; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + } + + .embed-description { + margin: 0; + font-size: 0.9375rem; + color: $grey-30; + line-height: 1.5; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + } + } diff --git a/src/lib/components/admin/AlbumListItem.svelte b/src/lib/components/admin/AlbumListItem.svelte index 09f8e9a..a1ab5fa 100644 --- a/src/lib/components/admin/AlbumListItem.svelte +++ b/src/lib/components/admin/AlbumListItem.svelte @@ -297,7 +297,7 @@ .dropdown-divider { height: 1px; - background-color: $grey-90; + background-color: $grey-80; margin: $unit-half 0; } diff --git a/src/lib/components/admin/DropdownMenu.svelte b/src/lib/components/admin/DropdownMenu.svelte index 833612d..d834927 100644 --- a/src/lib/components/admin/DropdownMenu.svelte +++ b/src/lib/components/admin/DropdownMenu.svelte @@ -123,7 +123,7 @@ .dropdown-divider { height: 1px; - background-color: $grey-90; + background-color: $grey-80; margin: $unit-half 0; } diff --git a/src/lib/components/admin/PostListItem.svelte b/src/lib/components/admin/PostListItem.svelte index 76c016c..0f43852 100644 --- a/src/lib/components/admin/PostListItem.svelte +++ b/src/lib/components/admin/PostListItem.svelte @@ -306,7 +306,7 @@ .dropdown-divider { height: 1px; - background-color: $grey-90; + background-color: $grey-80; margin: $unit-half 0; } diff --git a/src/lib/components/admin/ProjectListItem.svelte b/src/lib/components/admin/ProjectListItem.svelte index 86495c5..707b64d 100644 --- a/src/lib/components/admin/ProjectListItem.svelte +++ b/src/lib/components/admin/ProjectListItem.svelte @@ -256,7 +256,7 @@ .dropdown-divider { height: 1px; - background-color: $grey-90; + background-color: $grey-80; margin: $unit-half 0; } diff --git a/src/lib/components/admin/StatusDropdown.svelte b/src/lib/components/admin/StatusDropdown.svelte index 34f9129..fc86556 100644 --- a/src/lib/components/admin/StatusDropdown.svelte +++ b/src/lib/components/admin/StatusDropdown.svelte @@ -134,7 +134,7 @@ .dropdown-divider { height: 1px; - background-color: $grey-90; + background-color: $grey-80; margin: $unit-half 0; } diff --git a/src/lib/utils/extractEmbeds.ts b/src/lib/utils/extractEmbeds.ts new file mode 100644 index 0000000..c92a675 --- /dev/null +++ b/src/lib/utils/extractEmbeds.ts @@ -0,0 +1,79 @@ +// Extract URL embeds from Tiptap content +export interface ExtractedEmbed { + type: 'urlEmbed' | 'youtube' + url: string + title?: string + description?: string + image?: string + favicon?: string + siteName?: string + videoId?: string +} + +export function extractEmbeds(content: any): ExtractedEmbed[] { + if (!content || !content.content) return [] + + const embeds: ExtractedEmbed[] = [] + + // Helper to extract YouTube video ID + const getYouTubeVideoId = (url: string): string | null => { + const patterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, + /youtube\.com\/watch\?.*v=([^&\n?#]+)/ + ] + + for (const pattern of patterns) { + const match = url.match(pattern) + if (match && match[1]) { + return match[1] + } + } + return null + } + + // Recursive function to find embed nodes + const findEmbeds = (node: any) => { + if (node.type === 'urlEmbed' && node.attrs?.url) { + const url = node.attrs.url + const isYouTube = /(?:youtube\.com|youtu\.be)/.test(url) + + if (isYouTube) { + const videoId = getYouTubeVideoId(url) + if (videoId) { + embeds.push({ + type: 'youtube', + url, + videoId, + title: node.attrs.title, + description: node.attrs.description, + image: node.attrs.image, + favicon: node.attrs.favicon, + siteName: node.attrs.siteName + }) + } + } else { + embeds.push({ + type: 'urlEmbed', + url, + title: node.attrs.title, + description: node.attrs.description, + image: node.attrs.image, + favicon: node.attrs.favicon, + siteName: node.attrs.siteName + }) + } + } + + // Recursively check child nodes + if (node.content && Array.isArray(node.content)) { + node.content.forEach(findEmbeds) + } + } + + // Start searching from the root + if (content.content && Array.isArray(content.content)) { + content.content.forEach(findEmbeds) + } + + return embeds +} \ No newline at end of file