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) {
|
:global(.url-embed-link) {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: $grey-95;
|
flex-direction: column;
|
||||||
|
background: $grey-97;
|
||||||
border-radius: $card-corner-radius;
|
border-radius: $card-corner-radius;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid $grey-85;
|
border: 1px solid $grey-80;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: $grey-60;
|
border-color: $grey-80;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
text-decoration: none;
|
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) {
|
:global(.url-embed-image) {
|
||||||
flex-shrink: 0;
|
width: 100%;
|
||||||
width: 200px;
|
aspect-ratio: 2 / 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: $grey-90;
|
background: $grey-90;
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +350,7 @@
|
||||||
|
|
||||||
:global(.url-embed-text) {
|
:global(.url-embed-text) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: $unit-3x;
|
padding: $unit-2x $unit-3x $unit-3x;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit;
|
gap: $unit;
|
||||||
|
|
@ -358,8 +360,8 @@
|
||||||
:global(.url-embed-meta) {
|
:global(.url-embed-meta) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $unit;
|
gap: $unit-half;
|
||||||
font-size: 0.75rem;
|
font-size: 0.8125rem;
|
||||||
color: $grey-40;
|
color: $grey-40;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -373,11 +375,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.url-embed-title) {
|
:global(.url-embed-title) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 1.125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $grey-10;
|
color: $grey-10;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
|
@ -389,12 +392,12 @@
|
||||||
|
|
||||||
:global(.url-embed-description) {
|
:global(.url-embed-description) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.875rem;
|
font-size: 0.9375rem;
|
||||||
color: $grey-30;
|
color: $grey-30;
|
||||||
line-height: 1.4;
|
line-height: 1.5;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 3;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,17 +424,5 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import UniverseCard from './UniverseCard.svelte'
|
import UniverseCard from './UniverseCard.svelte'
|
||||||
import { getContentExcerpt } from '$lib/utils/content'
|
import { getContentExcerpt } from '$lib/utils/content'
|
||||||
|
import { extractEmbeds } from '$lib/utils/extractEmbeds'
|
||||||
import type { UniverseItem } from '../../routes/api/universe/+server'
|
import type { UniverseItem } from '../../routes/api/universe/+server'
|
||||||
|
|
||||||
let { post }: { post: UniverseItem } = $props()
|
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
|
// Check if content is truncated
|
||||||
const isContentTruncated = $derived(() => {
|
const isContentTruncated = $derived(() => {
|
||||||
if (post.content) {
|
if (post.content) {
|
||||||
|
|
@ -14,6 +19,16 @@
|
||||||
}
|
}
|
||||||
return false
|
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>
|
</script>
|
||||||
|
|
||||||
<UniverseCard item={post} type="post">
|
<UniverseCard item={post} type="post">
|
||||||
|
|
@ -29,6 +44,46 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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}
|
{#if post.postType === 'essay' && isContentTruncated}
|
||||||
<p>
|
<p>
|
||||||
<a href="/universe/{post.slug}" class="read-more" tabindex="-1">Continue reading</a>
|
<a href="/universe/{post.slug}" class="read-more" tabindex="-1">Continue reading</a>
|
||||||
|
|
@ -60,7 +115,7 @@
|
||||||
.link-preview {
|
.link-preview {
|
||||||
background: $grey-97;
|
background: $grey-97;
|
||||||
border: 1px solid $grey-90;
|
border: 1px solid $grey-90;
|
||||||
border-radius: $unit;
|
border-radius: $card-corner-radius;
|
||||||
padding: $unit-2x;
|
padding: $unit-2x;
|
||||||
margin-bottom: $unit-3x;
|
margin-bottom: $unit-3x;
|
||||||
|
|
||||||
|
|
@ -119,4 +174,116 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s ease;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@
|
||||||
|
|
||||||
.dropdown-divider {
|
.dropdown-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: $grey-90;
|
background-color: $grey-80;
|
||||||
margin: $unit-half 0;
|
margin: $unit-half 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
|
|
||||||
.dropdown-divider {
|
.dropdown-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: $grey-90;
|
background-color: $grey-80;
|
||||||
margin: $unit-half 0;
|
margin: $unit-half 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@
|
||||||
|
|
||||||
.dropdown-divider {
|
.dropdown-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: $grey-90;
|
background-color: $grey-80;
|
||||||
margin: $unit-half 0;
|
margin: $unit-half 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,7 @@
|
||||||
|
|
||||||
.dropdown-divider {
|
.dropdown-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: $grey-90;
|
background-color: $grey-80;
|
||||||
margin: $unit-half 0;
|
margin: $unit-half 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@
|
||||||
|
|
||||||
.dropdown-divider {
|
.dropdown-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: $grey-90;
|
background-color: $grey-80;
|
||||||
margin: $unit-half 0;
|
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