your personal website on atproto - mirror
blento.app
1<script lang="ts">
2 import { browser } from '$app/environment';
3 import { page } from '$app/state';
4 import { innerWidth } from 'svelte/reactivity/window';
5 import BaseCard from '$lib/cards/_base/BaseCard/BaseCard.svelte';
6 import Card from '$lib/cards/_base/Card/Card.svelte';
7 import { CardDefinitionsByType, getColor } from '$lib/cards';
8 import { getDescription, getImage, getName } from '$lib/helper';
9 import QRModalProvider from '$lib/components/qr/QRModalProvider.svelte';
10 import ImageViewerProvider from '$lib/components/image-viewer/ImageViewerProvider.svelte';
11 import type { WebsiteData } from '$lib/types';
12 import Context from './Context.svelte';
13 import Head from './Head.svelte';
14 import { setIsMobile } from './context';
15
16 let { data }: { data: WebsiteData } = $props();
17
18 let item = $derived(data.cards[0]);
19 let embeddedItem = $derived({
20 ...item,
21 x: 0,
22 y: 0,
23 mobileX: 0,
24 mobileY: 0
25 });
26
27 let isMobile = $derived((innerWidth.current ?? 1000) < 1024);
28 setIsMobile(() => isMobile);
29
30 const colors = {
31 base: 'bg-base-200/50 dark:bg-base-950/50',
32 accent: 'bg-accent-400 dark:bg-accent-500 accent',
33 transparent: 'bg-transparent'
34 } as Record<string, string>;
35
36 let color = $derived(getColor(item));
37 let backgroundClass = $derived(color ? (colors[color] ?? colors.accent) : colors.base);
38 let pageColorClass = $derived(
39 color !== 'accent' && item?.color !== 'base' && item?.color !== 'transparent' ? color : ''
40 );
41 let cardWidth = $derived(Math.max(isMobile ? item.mobileW : item.w, 1));
42 let cardHeight = $derived(Math.max(isMobile ? item.mobileH : item.h, 1));
43
44 let title = $derived.by(() => {
45 const label = item?.cardData?.label as string | undefined;
46 const cardName = CardDefinitionsByType[item?.cardType ?? '']?.name;
47
48 return label
49 ? `${label} • ${getName(data)}`
50 : cardName
51 ? `${cardName} • ${getName(data)}`
52 : getName(data);
53 });
54
55 let description = $derived(
56 (item?.cardData?.title as string | undefined) ||
57 (item?.cardData?.text as string | undefined) ||
58 getDescription(data)
59 );
60
61 const safeJson = (value: string) => JSON.stringify(value).replace(/</g, '\\u003c');
62
63 let themeMode = $derived.by(() => {
64 const theme = page.url.searchParams.get('theme');
65 return theme === 'dark' || theme === 'light' || theme === 'auto' ? theme : undefined;
66 });
67
68 let themeScript = $derived.by(() => {
69 if (!themeMode) return '';
70
71 return (
72 `<script>(function(){var theme=${safeJson(themeMode)};var el=document.documentElement;` +
73 `var apply=function(mode){el.classList.remove('dark','light');` +
74 `el.classList.add(mode==='auto'&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':mode==='auto'?'light':mode);};` +
75 `apply(theme);})();<` +
76 '/script>'
77 );
78 });
79
80 $effect(() => {
81 if (!browser || !themeMode) return;
82
83 const root = document.documentElement;
84 const previousHadDark = root.classList.contains('dark');
85 const previousHadLight = root.classList.contains('light');
86
87 const applyTheme = () => {
88 root.classList.remove('dark', 'light');
89
90 if (themeMode === 'auto') {
91 root.classList.add(
92 window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
93 );
94 return;
95 }
96
97 root.classList.add(themeMode);
98 };
99
100 applyTheme();
101
102 if (themeMode !== 'auto') {
103 return () => {
104 root.classList.remove('dark', 'light');
105 if (previousHadDark) root.classList.add('dark');
106 if (previousHadLight) root.classList.add('light');
107 };
108 }
109
110 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
111 mediaQuery.addEventListener('change', applyTheme);
112
113 return () => {
114 mediaQuery.removeEventListener('change', applyTheme);
115 root.classList.remove('dark', 'light');
116 if (previousHadDark) root.classList.add('dark');
117 if (previousHadLight) root.classList.add('light');
118 };
119 });
120</script>
121
122<Head
123 favicon={getImage(data.publication, data.did, 'icon') || data.profile.avatar}
124 {title}
125 {description}
126 accentColor={data.publication?.preferences?.accentColor}
127 baseColor={data.publication?.preferences?.baseColor}
128/>
129
130<svelte:head>
131 <meta name="robots" content="noindex" />
132 {@html themeScript}
133</svelte:head>
134
135<Context {data}>
136 <QRModalProvider />
137 <ImageViewerProvider />
138
139 <div class={[backgroundClass, pageColorClass, 'embed-page w-full']}>
140 <div class="embed-stage @container/grid">
141 <div
142 class="embed-content"
143 style={`--embed-ratio: ${cardWidth / cardHeight}; aspect-ratio: ${cardWidth} / ${cardHeight};`}
144 >
145 <BaseCard item={embeddedItem} fillPage>
146 <Card item={embeddedItem} />
147 </BaseCard>
148 </div>
149 </div>
150 </div>
151</Context>
152
153<style>
154 :global(html),
155 :global(body) {
156 min-height: 100%;
157 }
158
159 .embed-page {
160 min-height: 100vh;
161 }
162
163 .embed-stage {
164 min-height: 100vh;
165 display: flex;
166 align-items: center;
167 justify-content: center;
168 padding: clamp(12px, 3vw, 32px);
169 container-type: inline-size;
170 }
171
172 .embed-content {
173 width: min(100%, calc((100vh - clamp(24px, 6vw, 64px)) * var(--embed-ratio)));
174 max-width: calc(100vw - clamp(24px, 6vw, 64px));
175 max-height: calc(100vh - clamp(24px, 6vw, 64px));
176 }
177</style>