Fix styling of embeds in public site
This commit is contained in:
parent
fe30f9e9b2
commit
a468668858
8 changed files with 268 additions and 31 deletions
|
|
@ -318,24 +318,26 @@
|
|||
|
||||
:global(.url-embed-link) {
|
||||
display: flex;
|
||||
background: $grey-95;
|
||||
flex-direction: column;
|
||||
background: $grey-97;
|
||||
border-radius: $card-corner-radius;
|
||||
overflow: hidden;
|
||||
border: 1px solid $grey-85;
|
||||
border: 1px solid $grey-80;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-60;
|
||||
border-color: $grey-80;
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.url-embed-image) {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 1;
|
||||
overflow: hidden;
|
||||
background: $grey-90;
|
||||
}
|
||||
|
|
@ -348,7 +350,7 @@
|
|||
|
||||
:global(.url-embed-text) {
|
||||
flex: 1;
|
||||
padding: $unit-3x;
|
||||
padding: $unit-2x $unit-3x $unit-3x;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
|
@ -358,8 +360,8 @@
|
|||
:global(.url-embed-meta) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
font-size: 0.75rem;
|
||||
gap: $unit-half;
|
||||
font-size: 0.8125rem;
|
||||
color: $grey-40;
|
||||
}
|
||||
|
||||
|
|
@ -373,11 +375,12 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
:global(.url-embed-title) {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: $grey-10;
|
||||
line-height: 1.3;
|
||||
|
|
@ -389,12 +392,12 @@
|
|||
|
||||
:global(.url-embed-description) {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.9375rem;
|
||||
color: $grey-30;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
@ -421,17 +424,5 @@
|
|||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
// Mobile styles for URL embeds
|
||||
@media (max-width: 640px) {
|
||||
:global(.url-embed-link) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:global(.url-embed-image) {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
<script lang="ts">
|
||||
import UniverseCard from './UniverseCard.svelte'
|
||||
import { getContentExcerpt } from '$lib/utils/content'
|
||||
import { extractEmbeds } from '$lib/utils/extractEmbeds'
|
||||
import type { UniverseItem } from '../../routes/api/universe/+server'
|
||||
|
||||
let { post }: { post: UniverseItem } = $props()
|
||||
|
||||
// Extract embeds from content
|
||||
const embeds = $derived(post.content ? extractEmbeds(post.content) : [])
|
||||
const firstEmbed = $derived(embeds[0])
|
||||
|
||||
// Check if content is truncated
|
||||
const isContentTruncated = $derived(() => {
|
||||
if (post.content) {
|
||||
|
|
@ -14,6 +19,16 @@
|
|||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Helper to get domain from URL
|
||||
const getDomain = (url: string) => {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.hostname.replace('www.', '')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<UniverseCard item={post} type="post">
|
||||
|
|
@ -29,6 +44,46 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if firstEmbed}
|
||||
<div class="embed-preview">
|
||||
{#if firstEmbed.type === 'youtube' && firstEmbed.videoId}
|
||||
<div class="youtube-embed-preview">
|
||||
<div class="youtube-player">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/{firstEmbed.videoId}"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
title="YouTube video player"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<a href="/universe/{post.slug}" class="url-embed-preview" tabindex="-1">
|
||||
{#if firstEmbed.image}
|
||||
<div class="embed-image">
|
||||
<img src={firstEmbed.image} alt={firstEmbed.title || 'Link preview'} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="embed-text">
|
||||
<div class="embed-meta">
|
||||
{#if firstEmbed.favicon}
|
||||
<img src={firstEmbed.favicon} alt="" class="embed-favicon" />
|
||||
{/if}
|
||||
<span class="embed-domain">{firstEmbed.siteName || getDomain(firstEmbed.url)}</span>
|
||||
</div>
|
||||
{#if firstEmbed.title}
|
||||
<h3 class="embed-title">{firstEmbed.title}</h3>
|
||||
{/if}
|
||||
{#if firstEmbed.description}
|
||||
<p class="embed-description">{firstEmbed.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if post.postType === 'essay' && isContentTruncated}
|
||||
<p>
|
||||
<a href="/universe/{post.slug}" class="read-more" tabindex="-1">Continue reading</a>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@
|
|||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-90;
|
||||
background-color: $grey-80;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-90;
|
||||
background-color: $grey-80;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@
|
|||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-90;
|
||||
background-color: $grey-80;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@
|
|||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-90;
|
||||
background-color: $grey-80;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@
|
|||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-90;
|
||||
background-color: $grey-80;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
79
src/lib/utils/extractEmbeds.ts
Normal file
79
src/lib/utils/extractEmbeds.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue