updated services
This commit is contained in:
parent
e51f83aa8c
commit
0f5155811d
5 changed files with 743 additions and 636 deletions
|
|
@ -1,106 +1,106 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
Motion,
|
Motion,
|
||||||
useTransform,
|
useTransform,
|
||||||
AnimatePresence,
|
AnimatePresence,
|
||||||
useMotionValue,
|
useMotionValue,
|
||||||
useSpring,
|
useSpring,
|
||||||
} from "svelte-motion";
|
} from "svelte-motion";
|
||||||
|
|
||||||
export let items: {
|
export let items: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
url: string;
|
url: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
let hoveredIndex: number | null = null;
|
let hoveredIndex: number | null = null;
|
||||||
const springConfig = { stiffness: 100, damping: 5 };
|
const springConfig = { stiffness: 100, damping: 5 };
|
||||||
const x = useMotionValue(0); // going to set this value on mouse move
|
const x = useMotionValue(0); // going to set this value on mouse move
|
||||||
// rotate the tooltip
|
// rotate the tooltip
|
||||||
const rotate = useSpring(
|
const rotate = useSpring(
|
||||||
useTransform(x, [-100, 100], [-45, 45]),
|
useTransform(x, [-100, 100], [-45, 45]),
|
||||||
springConfig,
|
springConfig,
|
||||||
);
|
);
|
||||||
// translate the tooltip
|
// translate the tooltip
|
||||||
const translateX = useSpring(
|
const translateX = useSpring(
|
||||||
useTransform(x, [-100, 100], [-50, 50]),
|
useTransform(x, [-100, 100], [-50, 50]),
|
||||||
springConfig,
|
springConfig,
|
||||||
);
|
);
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const halfWidth = event.target?.offsetWidth / 2;
|
const halfWidth = event.target?.offsetWidth / 2;
|
||||||
x.set(event.offsetX - halfWidth); // set the x value, which is then used in transform and rotate
|
x.set(event.offsetX - halfWidth); // set the x value, which is then used in transform and rotate
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="group flex flex-row pl-2">
|
<div class="group flex flex-row pl-2">
|
||||||
{#each items as item, idx (item.name)}
|
{#each items as item, idx (item.name)}
|
||||||
<div class="relative mr-5">
|
<div class="relative mr-5">
|
||||||
<AnimatePresence show={true}>
|
<AnimatePresence show={true}>
|
||||||
{#if hoveredIndex === item.id}
|
{#if hoveredIndex === item.id}
|
||||||
<Motion
|
<Motion
|
||||||
let:motion
|
let:motion
|
||||||
initial={{ opacity: 0, y: 20, scale: 0.6 }}
|
initial={{ opacity: 0, y: 20, scale: 0.6 }}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
transition: {
|
transition: {
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 260,
|
stiffness: 260,
|
||||||
damping: 10,
|
damping: 10,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
exit={{ opacity: 0, y: 20, scale: 0.6 }}
|
exit={{ opacity: 0, y: 20, scale: 0.6 }}
|
||||||
style={{
|
style={{
|
||||||
translateX: translateX,
|
translateX: translateX,
|
||||||
rotate: rotate,
|
rotate: rotate,
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
use:motion
|
use:motion
|
||||||
id="tooltip-{item.id}"
|
id="tooltip-{item.id}"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
class="absolute left-1/2 -top-10 z-50 flex -translate-x-1/2 flex-col items-center justify-center rounded-md bg-gray-700 px-4 py-2 text-xs shadow-xl"
|
class="absolute left-1/2 -top-10 z-50 flex -translate-x-1/2 flex-col items-center justify-center rounded-md bg-gray-700 px-4 py-2 text-xs shadow-xl"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-x-10 -bottom-px z-30 h-px w-[20%] bg-linear-to-r from-transparent via-emerald-500 to-transparent"
|
class="absolute inset-x-10 -bottom-px z-30 h-px w-[20%] bg-linear-to-r from-transparent via-emerald-500 to-transparent"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="absolute -bottom-px left-10 z-30 h-px w-[40%] bg-linear-to-r from-transparent via-sky-500 to-transparent"
|
class="absolute -bottom-px left-10 z-30 h-px w-[40%] bg-linear-to-r from-transparent via-sky-500 to-transparent"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="relative z-30 whitespace-nowrap text-base font-bold text-white"
|
class="relative z-30 whitespace-nowrap text-base font-bold text-white"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Motion>
|
</Motion>
|
||||||
{/if}
|
{/if}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
<a
|
<a
|
||||||
href={item.url}
|
href={item.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="relative m-0 h-14 w-14 cursor-pointer rounded-full border-0 bg-transparent p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
|
class="relative m-0 h-14 w-14 cursor-pointer rounded-full border-0 bg-transparent p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
|
||||||
on:mousemove={handleMouseMove}
|
on:mousemove={handleMouseMove}
|
||||||
on:mouseenter={() => (hoveredIndex = item.id)}
|
on:mouseenter={() => (hoveredIndex = item.id)}
|
||||||
on:mouseleave={() => (hoveredIndex = null)}
|
on:mouseleave={() => (hoveredIndex = null)}
|
||||||
on:focus={() => (hoveredIndex = item.id)}
|
on:focus={() => (hoveredIndex = item.id)}
|
||||||
on:blur={() => (hoveredIndex = null)}
|
on:blur={() => (hoveredIndex = null)}
|
||||||
aria-describedby={hoveredIndex === item.id
|
aria-describedby={hoveredIndex === item.id
|
||||||
? `tooltip-${item.id}`
|
? `tooltip-${item.id}`
|
||||||
: undefined}
|
: undefined}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
height={100}
|
height={100}
|
||||||
width={100}
|
width={100}
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
class="m-0! h-10 w-10 rounded-lg object-cover object-top p-0!"
|
class="m-0! h-10 w-10 rounded-lg object-cover object-top p-0!"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -83,3 +83,4 @@ export async function updatePlatforms(
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.platforms;
|
return data.platforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,275 +1,316 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from "svelte";
|
||||||
import {
|
import {
|
||||||
clearStoredToken,
|
clearStoredToken,
|
||||||
fetchAdminState,
|
fetchAdminState,
|
||||||
getStoredToken,
|
getStoredToken,
|
||||||
updateContent,
|
updateContent,
|
||||||
updatePlatforms,
|
updatePlatforms,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
} from '../lib/api'
|
} from "../lib/api";
|
||||||
import type { Platform } from '../lib/types'
|
import type { Platform } from "../lib/types";
|
||||||
|
|
||||||
let authed = $state(false)
|
let authed = $state(false);
|
||||||
let loading = $state(true)
|
let loading = $state(true);
|
||||||
let message = $state('')
|
let message = $state("");
|
||||||
let error = $state('')
|
let error = $state("");
|
||||||
let username = $state('')
|
let username = $state("");
|
||||||
let password = $state('')
|
let password = $state("");
|
||||||
|
|
||||||
let content = $state<Record<string, string>>({})
|
let content = $state<Record<string, string>>({});
|
||||||
let platforms = $state<Platform[]>([])
|
let platforms = $state<Platform[]>([]);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (getStoredToken()) {
|
if (getStoredToken()) {
|
||||||
await loadState()
|
await loadState();
|
||||||
} else {
|
} else {
|
||||||
loading = false
|
loading = false;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
function titleCase(key: string): string {
|
function titleCase(key: string): string {
|
||||||
return key
|
return key
|
||||||
.split('_')
|
.split("_")
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ')
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadState() {
|
async function loadState() {
|
||||||
loading = true
|
loading = true;
|
||||||
error = ''
|
error = "";
|
||||||
try {
|
try {
|
||||||
const state = await fetchAdminState()
|
const state = await fetchAdminState();
|
||||||
content = state.content
|
content = state.content;
|
||||||
platforms = state.platforms
|
platforms = state.platforms;
|
||||||
authed = true
|
authed = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authed = false
|
authed = false;
|
||||||
error = err instanceof Error ? err.message : 'Unable to load admin state'
|
error =
|
||||||
} finally {
|
err instanceof Error
|
||||||
loading = false
|
? err.message
|
||||||
}
|
: "Unable to load admin state";
|
||||||
}
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin(event: SubmitEvent) {
|
async function handleLogin(event: SubmitEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault();
|
||||||
error = ''
|
error = "";
|
||||||
message = ''
|
message = "";
|
||||||
|
|
||||||
if (await verifyAdmin(username, password)) {
|
if (await verifyAdmin(username, password)) {
|
||||||
password = ''
|
password = "";
|
||||||
await loadState()
|
await loadState();
|
||||||
} else {
|
} else {
|
||||||
error = 'Invalid credentials'
|
error = "Invalid credentials";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
clearStoredToken()
|
clearStoredToken();
|
||||||
authed = false
|
authed = false;
|
||||||
content = {}
|
content = {};
|
||||||
platforms = []
|
platforms = [];
|
||||||
message = ''
|
message = "";
|
||||||
error = ''
|
error = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveContent() {
|
async function handleSaveContent() {
|
||||||
error = ''
|
error = "";
|
||||||
message = ''
|
message = "";
|
||||||
try {
|
try {
|
||||||
content = await updateContent(content)
|
content = await updateContent(content);
|
||||||
message = 'Content saved'
|
message = "Content saved";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : 'Unable to save content'
|
error =
|
||||||
}
|
err instanceof Error ? err.message : "Unable to save content";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSavePlatforms() {
|
async function handleSavePlatforms() {
|
||||||
error = ''
|
error = "";
|
||||||
message = ''
|
message = "";
|
||||||
try {
|
try {
|
||||||
const payload = platforms.map((platform) => ({
|
const payload = platforms.map((platform) => ({
|
||||||
...platform,
|
...platform,
|
||||||
followers: Number(platform.followers) || 0,
|
followers: Number(platform.followers) || 0,
|
||||||
}))
|
}));
|
||||||
platforms = await updatePlatforms(payload)
|
platforms = await updatePlatforms(payload);
|
||||||
message = 'Platforms saved'
|
message = "Platforms saved";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : 'Unable to save platforms'
|
error =
|
||||||
}
|
err instanceof Error ? err.message : "Unable to save platforms";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="mx-auto w-full max-w-[920px] px-6 pt-12 pb-[72px]">
|
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||||
<header class="flex items-start justify-between gap-6">
|
<header class="flex items-start justify-between gap-6">
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-3 text-xs font-semibold tracking-[0.14em] uppercase text-gray-500 dark:text-gray-400">CMS</p>
|
<p
|
||||||
<h1 class="text-[clamp(36px,8vw,52px)] leading-[1.05] font-semibold tracking-tight">
|
class="mb-3 text-xs font-semibold tracking-[0.14em] uppercase text-gray-500 dark:text-gray-400"
|
||||||
RythonDev media kit
|
>
|
||||||
</h1>
|
CMS
|
||||||
</div>
|
</p>
|
||||||
<div class="flex items-center gap-3">
|
<h1
|
||||||
<a class="text-sm text-gray-500 hover:text-neutral-900 dark:text-gray-400 dark:hover:text-neutral-50" href="#/">
|
class="text-[clamp(36px,8vw,52px)] leading-[1.05] font-semibold tracking-tight"
|
||||||
View media kit
|
>
|
||||||
</a>
|
RythonDev media kit
|
||||||
{#if authed}
|
</h1>
|
||||||
<button
|
</div>
|
||||||
class="cursor-pointer rounded-[10px] border border-gray-200 bg-transparent px-4 py-2.5 text-sm font-medium text-neutral-900 transition-opacity hover:opacity-85 dark:border-zinc-800 dark:text-neutral-50"
|
<div class="flex items-center gap-3">
|
||||||
onclick={handleLogout}
|
<a
|
||||||
>
|
class="text-sm text-gray-500 hover:text-neutral-900 dark:text-gray-400 dark:hover:text-neutral-50"
|
||||||
Log out
|
href="#/"
|
||||||
</button>
|
>
|
||||||
{/if}
|
View media kit
|
||||||
</div>
|
</a>
|
||||||
</header>
|
{#if authed}
|
||||||
|
<button
|
||||||
|
class="cursor-pointer rounded-[10px] border border-gray-200 bg-transparent px-4 py-2.5 text-sm font-medium text-neutral-900 transition-opacity hover:opacity-85 dark:border-zinc-800 dark:text-neutral-50"
|
||||||
|
onclick={handleLogout}
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="py-8 text-gray-500 dark:text-gray-400">Loading…</div>
|
<div class="py-8 text-gray-500 dark:text-gray-400">Loading…</div>
|
||||||
{:else if !authed}
|
{:else if !authed}
|
||||||
<form class="mt-10 max-w-[400px]" onsubmit={handleLogin}>
|
<form class="mt-10 max-w-100" onsubmit={handleLogin}>
|
||||||
<h2 class="mb-2 text-xl font-semibold tracking-tight">Sign in</h2>
|
<h2 class="mb-2 text-xl font-semibold tracking-tight">Sign in</h2>
|
||||||
<p class="text-gray-500 dark:text-gray-400">Enter the admin credentials configured for this instance.</p>
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
Enter the admin credentials configured for this instance.
|
||||||
|
</p>
|
||||||
|
|
||||||
<label class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400">
|
<label
|
||||||
<span>Username</span>
|
class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400"
|
||||||
<input
|
>
|
||||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
<span>Username</span>
|
||||||
bind:value={username}
|
<input
|
||||||
autocomplete="username"
|
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
required
|
bind:value={username}
|
||||||
/>
|
autocomplete="username"
|
||||||
</label>
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400">
|
<label
|
||||||
<span>Password</span>
|
class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400"
|
||||||
<input
|
>
|
||||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
<span>Password</span>
|
||||||
bind:value={password}
|
<input
|
||||||
type="password"
|
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
autocomplete="current-password"
|
bind:value={password}
|
||||||
required
|
type="password"
|
||||||
/>
|
autocomplete="current-password"
|
||||||
</label>
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400">
|
<div
|
||||||
{error}
|
class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||||
</div>
|
>
|
||||||
{/if}
|
{error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="cursor-pointer rounded-[10px] border border-transparent bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-85 dark:bg-neutral-50 dark:text-neutral-950"
|
class="cursor-pointer rounded-[10px] border border-transparent bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-85 dark:bg-neutral-50 dark:text-neutral-950"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2 md:items-start">
|
<div class="mt-10 grid grid-cols-1 gap-8 md:grid-cols-2 md:items-start">
|
||||||
<section class="border-t border-gray-200 pt-8 dark:border-zinc-800">
|
<section class="border-t border-gray-200 pt-8 dark:border-zinc-800">
|
||||||
<div class="mb-5 flex items-center justify-between gap-4">
|
<div class="mb-5 flex items-center justify-between gap-4">
|
||||||
<h2 class="text-xl font-semibold tracking-tight">Text content</h2>
|
<h2 class="text-xl font-semibold tracking-tight">
|
||||||
<button
|
Text content
|
||||||
class="cursor-pointer rounded-[10px] border border-transparent bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-85 dark:bg-neutral-50 dark:text-neutral-950"
|
</h2>
|
||||||
onclick={handleSaveContent}
|
<button
|
||||||
>
|
class="cursor-pointer rounded-[10px] border border-transparent bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-85 dark:bg-neutral-50 dark:text-neutral-950"
|
||||||
Save content
|
onclick={handleSaveContent}
|
||||||
</button>
|
>
|
||||||
</div>
|
Save content
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
{#each Object.keys(content) as key}
|
{#each Object.keys(content) as key}
|
||||||
<label class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400">
|
<label
|
||||||
<span>{titleCase(key)}</span>
|
class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400"
|
||||||
{#if key === 'about'}
|
>
|
||||||
<textarea
|
<span>{titleCase(key)}</span>
|
||||||
class="w-full resize-y rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
{#if key === "about" || key === "services"}
|
||||||
rows="4"
|
<textarea
|
||||||
bind:value={content[key]}
|
class="w-full resize-y rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
></textarea>
|
rows={key === "services" ? 6 : 4}
|
||||||
{:else if key === 'profile_image_url'}
|
bind:value={content[key]}></textarea>
|
||||||
<div class="flex items-start gap-4">
|
{:else if key === "profile_image_url"}
|
||||||
<input
|
<div class="flex items-start gap-4">
|
||||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
<input
|
||||||
bind:value={content[key]}
|
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
/>
|
bind:value={content[key]}
|
||||||
{#if content[key]}
|
/>
|
||||||
<img
|
{#if content[key]}
|
||||||
class="h-16 w-16 shrink-0 rounded-full border border-gray-200 object-cover dark:border-zinc-800"
|
<img
|
||||||
src={content[key]}
|
class="h-16 w-16 shrink-0 rounded-full border border-gray-200 object-cover dark:border-zinc-800"
|
||||||
alt="Profile preview"
|
src={content[key]}
|
||||||
/>
|
alt="Profile preview"
|
||||||
{/if}
|
/>
|
||||||
</div>
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<input
|
{:else}
|
||||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
<input
|
||||||
bind:value={content[key]}
|
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
/>
|
bind:value={content[key]}
|
||||||
{/if}
|
/>
|
||||||
</label>
|
{/if}
|
||||||
{/each}
|
</label>
|
||||||
</div>
|
{/each}
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="border-t border-gray-200 pt-8 dark:border-zinc-800">
|
<section class="border-t border-gray-200 pt-8 dark:border-zinc-800">
|
||||||
<div class="mb-5 flex items-center justify-between gap-4">
|
<div class="mb-5 flex items-center justify-between gap-4">
|
||||||
<h2 class="text-xl font-semibold tracking-tight">Follower counts</h2>
|
<h2 class="text-xl font-semibold tracking-tight">
|
||||||
<button
|
Follower counts
|
||||||
class="cursor-pointer rounded-[10px] border border-transparent bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-85 dark:bg-neutral-50 dark:text-neutral-950"
|
</h2>
|
||||||
onclick={handleSavePlatforms}
|
<button
|
||||||
>
|
class="cursor-pointer rounded-[10px] border border-transparent bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-85 dark:bg-neutral-50 dark:text-neutral-950"
|
||||||
Save platforms
|
onclick={handleSavePlatforms}
|
||||||
</button>
|
>
|
||||||
</div>
|
Save platforms
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-5">
|
<div
|
||||||
{#each platforms as platform, index (platform.name)}
|
class="grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-5"
|
||||||
<div
|
>
|
||||||
class="rounded-2xl border border-gray-200 bg-neutral-50 p-5 dark:border-zinc-800 dark:bg-neutral-900"
|
{#each platforms as platform, index (platform.name)}
|
||||||
>
|
<div
|
||||||
<div class="mb-4 text-sm font-semibold">{titleCase(platform.name)}</div>
|
class="rounded-2xl border border-gray-200 bg-neutral-50 p-5 dark:border-zinc-800 dark:bg-neutral-900"
|
||||||
<div class="grid grid-cols-1 gap-4">
|
>
|
||||||
<label class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400">
|
<div class="mb-4 text-sm font-semibold">
|
||||||
<span>Handle</span>
|
{titleCase(platform.name)}
|
||||||
<input
|
</div>
|
||||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
<div class="grid grid-cols-1 gap-4">
|
||||||
bind:value={platform.handle}
|
<label
|
||||||
/>
|
class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400"
|
||||||
</label>
|
>
|
||||||
<label class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400">
|
<span>Handle</span>
|
||||||
<span>URL</span>
|
<input
|
||||||
<input
|
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
bind:value={platform.handle}
|
||||||
bind:value={platform.url}
|
/>
|
||||||
/>
|
</label>
|
||||||
</label>
|
<label
|
||||||
<label class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400">
|
class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400"
|
||||||
<span>Followers</span>
|
>
|
||||||
<input
|
<span>URL</span>
|
||||||
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
<input
|
||||||
type="number"
|
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
min="0"
|
bind:value={platform.url}
|
||||||
step="1"
|
/>
|
||||||
bind:value={platform.followers}
|
</label>
|
||||||
/>
|
<label
|
||||||
</label>
|
class="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400"
|
||||||
</div>
|
>
|
||||||
</div>
|
<span>Followers</span>
|
||||||
{/each}
|
<input
|
||||||
</div>
|
class="w-full rounded-[10px] border border-gray-200 bg-white px-3 py-2.5 text-neutral-900 outline-none focus:border-violet-600 dark:border-zinc-800 dark:bg-neutral-950 dark:text-neutral-50 dark:focus:border-violet-400"
|
||||||
</section>
|
type="number"
|
||||||
</div>
|
min="0"
|
||||||
|
step="1"
|
||||||
|
bind:value={platform.followers}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if message}
|
{#if message}
|
||||||
<div class="mt-6 rounded-[10px] bg-green-600/10 px-4 py-3 text-sm text-green-600 dark:text-green-400">
|
<div
|
||||||
{message}
|
class="mt-6 rounded-[10px] bg-green-600/10 px-4 py-3 text-sm text-green-600 dark:text-green-400"
|
||||||
</div>
|
>
|
||||||
{/if}
|
{message}
|
||||||
{#if error}
|
</div>
|
||||||
<div class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400">
|
{/if}
|
||||||
{error}
|
{#if error}
|
||||||
</div>
|
<div
|
||||||
{/if}
|
class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||||
{/if}
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,196 +1,231 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { fetchMediaKit } from "../lib/api";
|
import { fetchMediaKit } from "../lib/api";
|
||||||
import type { MediaKit } from "../lib/types";
|
import type { MediaKit } from "../lib/types";
|
||||||
import Card from "../components/Card.svelte";
|
import Card from "../components/Card.svelte";
|
||||||
import Collabs from "../components/Collabs.svelte";
|
import Collabs from "../components/Collabs.svelte";
|
||||||
|
|
||||||
let data = $state<MediaKit | null>(null);
|
let data = $state<MediaKit | null>(null);
|
||||||
let error = $state("");
|
let error = $state("");
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
data = await fetchMediaKit();
|
data = await fetchMediaKit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : "Something went wrong";
|
error = err instanceof Error ? err.message : "Something went wrong";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = $derived(data?.content ?? {});
|
let content = $derived(data?.content ?? {});
|
||||||
let platforms = $derived(data?.platforms ?? []);
|
let platforms = $derived(data?.platforms ?? []);
|
||||||
let twitch = $derived(data?.twitch);
|
let services = $derived(
|
||||||
// let twitchLogin = $derived(twitch?.login ?? "");
|
(content.services ?? "")
|
||||||
|
.split("\n")
|
||||||
|
.map((service) => service.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 6),
|
||||||
|
);
|
||||||
|
let twitch = $derived(data?.twitch);
|
||||||
|
// let twitchLogin = $derived(twitch?.login ?? "");
|
||||||
|
|
||||||
type platformDecoType = {
|
type platformDecoType = {
|
||||||
image: string;
|
image: string;
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
let platformImageMap: Record<string, platformDecoType> = {
|
let platformImageMap: Record<string, platformDecoType> = {
|
||||||
twitch: {
|
twitch: {
|
||||||
image: "/images/logos/twitch.svg",
|
image: "/images/logos/twitch.svg",
|
||||||
color: "#8E60F5",
|
color: "#8E60F5",
|
||||||
},
|
},
|
||||||
youtube: {
|
youtube: {
|
||||||
image: "/images/logos/youtube.svg",
|
image: "/images/logos/youtube.svg",
|
||||||
color: "#FF0000",
|
color: "#FF0000",
|
||||||
},
|
},
|
||||||
instagram: {
|
instagram: {
|
||||||
image: "/images/logos/instagram.svg",
|
image: "/images/logos/instagram.svg",
|
||||||
color: "#E1306C",
|
color: "#E1306C",
|
||||||
},
|
},
|
||||||
tiktok: {
|
tiktok: {
|
||||||
image: "/images/logos/tiktok.svg",
|
image: "/images/logos/tiktok.svg",
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
function formatCount(value: number | undefined | null): string {
|
function formatCount(value: number | undefined | null): string {
|
||||||
if (value == null) return "—";
|
if (value == null) return "—";
|
||||||
if (value >= 1_000_000) {
|
if (value >= 1_000_000) {
|
||||||
const millions = value / 1_000_000;
|
const millions = value / 1_000_000;
|
||||||
return `${millions >= 100 ? Math.round(millions) : millions.toFixed(1).replace(/\.0$/, "")}M`;
|
return `${millions >= 100 ? Math.round(millions) : millions.toFixed(1).replace(/\.0$/, "")}M`;
|
||||||
}
|
}
|
||||||
if (value >= 1_000) {
|
if (value >= 1_000) {
|
||||||
const thousands = value / 1_000;
|
const thousands = value / 1_000;
|
||||||
return `${thousands >= 100 ? Math.round(thousands) : thousands.toFixed(1).replace(/\.0$/, "")}K`;
|
return `${thousands >= 100 ? Math.round(thousands) : thousands.toFixed(1).replace(/\.0$/, "")}K`;
|
||||||
}
|
}
|
||||||
return value.toLocaleString();
|
return value.toLocaleString();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||||
<div
|
<div
|
||||||
class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{:else if !data}
|
{:else if !data}
|
||||||
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||||
<div class="py-8 text-gray-500 dark:text-gray-400">Loading…</div>
|
<div class="py-8 text-gray-500 dark:text-gray-400">Loading…</div>
|
||||||
</main>
|
</main>
|
||||||
{:else}
|
{:else}
|
||||||
<main class="grid grid-cols-2 max-w-6xl mx-auto pt-20 pb-10 gap-10">
|
<main class="grid grid-cols-2 max-w-6xl mx-auto pt-20 pb-10 gap-10">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="px-5">
|
<div class="px-5">
|
||||||
<div class="flex justify-end items-start">
|
<div class="flex justify-end items-start">
|
||||||
{#if content.profile_image_url}
|
{#if content.profile_image_url}
|
||||||
<img
|
<img
|
||||||
class="shrink-0 rounded-xl object-cover shadow-cyan-300 shadow-lg"
|
class="shrink-0 rounded-xl object-cover shadow-cyan-300 shadow-lg"
|
||||||
src={content.profile_image_url}
|
src={content.profile_image_url}
|
||||||
alt={content.brand_name ?? "Profile image"}
|
alt={content.brand_name ?? "Profile image"}
|
||||||
width="300"
|
width="300"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 grid grid-cols-2 gap-4">
|
<div class="mt-6 grid grid-cols-2 gap-4">
|
||||||
<!-- Twitch -->
|
<!-- Twitch -->
|
||||||
<Card
|
<Card
|
||||||
className="flex flex-col gap-1 rounded-2xl border p-5 transition-colors border-zinc-800 bg-neutral-900"
|
className="flex flex-col gap-1 rounded-2xl border p-5 transition-colors border-zinc-800 bg-neutral-900"
|
||||||
href={`https://twitch.tv/${twitch?.login}`}
|
href={`https://twitch.tv/${twitch?.login}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
color={platformImageMap["twitch"].color}
|
color={platformImageMap["twitch"].color}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row justify-start items-start gap-3"
|
class="flex flex-row justify-start items-start gap-3"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={platformImageMap["twitch"].image}
|
src={platformImageMap["twitch"].image}
|
||||||
alt="Twitch"
|
alt="Twitch"
|
||||||
width="40"
|
width="40"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="text-sm font-semibold tracking-[0.08em] uppercase text-white"
|
class="text-sm font-semibold tracking-[0.08em] uppercase text-white"
|
||||||
>
|
>
|
||||||
Twitch
|
Twitch
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span class="text-base"
|
<span class="text-base"
|
||||||
>{twitch?.display_name}</span
|
>{twitch?.display_name}</span
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mt-2 text-2xl font-semibold tracking-tight"
|
class="mt-2 text-2xl font-semibold tracking-tight"
|
||||||
>
|
>
|
||||||
{formatCount(twitch?.followers)}
|
{formatCount(twitch?.followers)}
|
||||||
<span class="text-sm">followers</span>
|
<span class="text-sm">followers</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{#each platforms as platform (platform.name)}
|
{#each platforms as platform (platform.name)}
|
||||||
<Card
|
<Card
|
||||||
className="flex flex-col gap-1 rounded-2xl border p-5 transition-colors border-zinc-800 bg-neutral-900"
|
className="flex flex-col gap-1 rounded-2xl border p-5 transition-colors border-zinc-800 bg-neutral-900"
|
||||||
href={platform.url}
|
href={platform.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
color={platformImageMap[platform.name]?.color}
|
color={platformImageMap[platform.name]?.color}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-row justify-start items-start gap-3"
|
class="flex flex-row justify-start items-start gap-3"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={platformImageMap[platform.name].image}
|
src={platformImageMap[platform.name].image}
|
||||||
alt="Twitch"
|
alt="Twitch"
|
||||||
width="48"
|
width="48"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="text-sm font-semibold tracking-[0.08em] uppercase text-white"
|
class="text-sm font-semibold tracking-[0.08em] uppercase text-white"
|
||||||
>
|
>
|
||||||
{platform.name}
|
{platform.name}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span class="text-base"
|
<span class="text-base"
|
||||||
>{platform.handle}</span
|
>{platform.handle}</span
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mt-2 text-2xl font-semibold tracking-tight"
|
class="mt-2 text-2xl font-semibold tracking-tight"
|
||||||
>
|
>
|
||||||
{formatCount(platform.followers)}
|
{formatCount(platform.followers)}
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
{#if platform.name != "youtube"}
|
{#if platform.name != "youtube"}
|
||||||
followers
|
followers
|
||||||
{:else}
|
{:else}
|
||||||
subscribers
|
subscribers
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 2nd column -->
|
<!-- 2nd column -->
|
||||||
<div class="flex flex-col justify-start items-start gap-5">
|
<div class="flex flex-col justify-start items-start gap-5">
|
||||||
<div
|
<div
|
||||||
class="border-cyan-300 border-2 rounded-lg translate-y-2 w-fit"
|
class="border-cyan-300 border-2 rounded-lg translate-y-2 w-fit"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="-translate-x-20 px-5 py-5 text-4xl bg-cyan-300 text-black rounded-xl -translate-y-2"
|
class="-translate-x-20 px-5 py-5 text-4xl bg-cyan-300 text-black rounded-xl -translate-y-2"
|
||||||
>
|
>
|
||||||
Hi there, I'm <br />
|
Hi there, I'm <br />
|
||||||
<b>RythonDev</b>
|
<b>RythonDev</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{#if content.tagline}
|
||||||
<div class="mt-5">
|
<p class="text-xl px-5 text-cyan-300 pb-2">
|
||||||
<div class="text-white font-bold text-4xl mb-2">About Me</div>
|
{content.tagline}
|
||||||
<p>
|
</p>
|
||||||
{content.about ?? ""}
|
{/if}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div class="mt-5">
|
||||||
<div>
|
<div class="text-white font-bold text-4xl mb-2">About Me</div>
|
||||||
<div class="text-white font-bold text-4xl mb-2">
|
<p>
|
||||||
Collaborations
|
{content.about ?? ""}
|
||||||
</div>
|
</p>
|
||||||
<Collabs />
|
</div>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<div class="text-white font-bold text-4xl mb-2">
|
||||||
</main>
|
Collaborations
|
||||||
|
</div>
|
||||||
|
<Collabs />
|
||||||
|
</div>
|
||||||
|
{#if services.length}
|
||||||
|
<section class="w-full">
|
||||||
|
<div class="mb-3 text-4xl font-bold text-white">
|
||||||
|
Services
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-x-8">
|
||||||
|
<ul
|
||||||
|
class="list-disc space-y-2 pl-5 marker:text-cyan-300"
|
||||||
|
>
|
||||||
|
{#each services.slice(0, 3) as service}
|
||||||
|
<li>{service}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<ul
|
||||||
|
class="list-disc space-y-2 pl-5 marker:text-cyan-300"
|
||||||
|
>
|
||||||
|
{#each services.slice(3, 6) as service}
|
||||||
|
<li>{service}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,147 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from "svelte";
|
||||||
import { fetchMediaKit } from '../lib/api'
|
import { fetchMediaKit } from "../lib/api";
|
||||||
import type { MediaKit } from '../lib/types'
|
import type { MediaKit } from "../lib/types";
|
||||||
|
|
||||||
let data = $state<MediaKit | null>(null)
|
let data = $state<MediaKit | null>(null);
|
||||||
let error = $state('')
|
let error = $state("");
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
data = await fetchMediaKit()
|
data = await fetchMediaKit();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : 'Something went wrong'
|
error = err instanceof Error ? err.message : "Something went wrong";
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const content = $derived(data?.content ?? {})
|
const content = $derived(data?.content ?? {});
|
||||||
const platforms = $derived(data?.platforms ?? [])
|
const platforms = $derived(data?.platforms ?? []);
|
||||||
const twitch = $derived(data?.twitch ?? null)
|
const twitch = $derived(data?.twitch ?? null);
|
||||||
const twitchLogin = $derived(twitch?.login ?? '')
|
const twitchLogin = $derived(twitch?.login ?? "");
|
||||||
|
|
||||||
function formatCount(value: number | undefined | null): string {
|
function formatCount(value: number | undefined | null): string {
|
||||||
if (value == null) return '—'
|
if (value == null) return "—";
|
||||||
if (value >= 1_000_000) {
|
if (value >= 1_000_000) {
|
||||||
const millions = value / 1_000_000
|
const millions = value / 1_000_000;
|
||||||
return `${millions >= 100 ? Math.round(millions) : millions.toFixed(1).replace(/\.0$/, '')}M`
|
return `${millions >= 100 ? Math.round(millions) : millions.toFixed(1).replace(/\.0$/, "")}M`;
|
||||||
}
|
}
|
||||||
if (value >= 1_000) {
|
if (value >= 1_000) {
|
||||||
const thousands = value / 1_000
|
const thousands = value / 1_000;
|
||||||
return `${thousands >= 100 ? Math.round(thousands) : thousands.toFixed(1).replace(/\.0$/, '')}K`
|
return `${thousands >= 100 ? Math.round(thousands) : thousands.toFixed(1).replace(/\.0$/, "")}K`;
|
||||||
}
|
}
|
||||||
return value.toLocaleString()
|
return value.toLocaleString();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||||
<div class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400">
|
<div
|
||||||
{error}
|
class="mt-6 rounded-[10px] bg-red-600/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
|
||||||
</div>
|
>
|
||||||
</main>
|
{error}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
{:else if !data}
|
{:else if !data}
|
||||||
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
<main class="mx-auto w-full max-w-230 px-6 pt-12 pb-18">
|
||||||
<div class="py-8 text-gray-500 dark:text-gray-400">Loading…</div>
|
<div class="py-8 text-gray-500 dark:text-gray-400">Loading…</div>
|
||||||
</main>
|
</main>
|
||||||
{:else}
|
{:else}
|
||||||
<main class="grid grid-cols-2">
|
<main class="grid grid-cols-2">
|
||||||
<div class="flex flex-col items-start gap-6 pt-6 pb-2 sm:flex-row sm:items-center">
|
<div
|
||||||
{#if content.profile_image_url}
|
class="flex flex-col items-start gap-6 pt-6 pb-2 sm:flex-row sm:items-center"
|
||||||
<img
|
>
|
||||||
class="h-24 w-24 shrink-0 rounded-md object-cover"
|
{#if content.profile_image_url}
|
||||||
src={content.profile_image_url}
|
<img
|
||||||
alt={content.brand_name ?? 'Profile image'}
|
class="h-24 w-24 shrink-0 rounded-md object-cover"
|
||||||
/>
|
src={content.profile_image_url}
|
||||||
{/if}
|
alt={content.brand_name ?? "Profile image"}
|
||||||
<div>
|
/>
|
||||||
<h1 class="text-[clamp(36px,8vw,52px)] leading-[1.05] font-semibold tracking-tight">
|
{/if}
|
||||||
{content.brand_name ?? 'RythonDev'}
|
<div>
|
||||||
</h1>
|
<h1
|
||||||
{#if content.tagline}
|
class="text-[clamp(36px,8vw,52px)] leading-[1.05] font-semibold tracking-tight"
|
||||||
<p class="mt-4 text-xl text-gray-500 dark:text-gray-400">{content.tagline}</p>
|
>
|
||||||
{/if}
|
{content.brand_name ?? "RythonDev"}
|
||||||
</div>
|
</h1>
|
||||||
</div>
|
{#if content.tagline}
|
||||||
|
<p class="mt-4 text-xl text-gray-500 dark:text-gray-400">
|
||||||
|
{content.tagline}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="mt-14 border-t border-gray-200 pt-8 dark:border-zinc-800">
|
<section
|
||||||
<h2 class="text-xl font-semibold tracking-tight">About</h2>
|
class="mt-14 border-t border-gray-200 pt-8 dark:border-zinc-800"
|
||||||
<p class="mt-4 max-w-160 text-lg leading-relaxed text-gray-500 dark:text-gray-400">
|
>
|
||||||
{content.about ?? ''}
|
<h2 class="text-xl font-semibold tracking-tight">About</h2>
|
||||||
</p>
|
<p
|
||||||
{#if content.email}
|
class="mt-4 max-w-160 text-lg leading-relaxed text-gray-500 dark:text-gray-400"
|
||||||
<a
|
>
|
||||||
class="mt-4 inline-block text-violet-600 dark:text-violet-400"
|
{content.about ?? ""}
|
||||||
href="mailto:{content.email}">{content.email}</a
|
</p>
|
||||||
>
|
{#if content.email}
|
||||||
{/if}
|
<a
|
||||||
</section>
|
class="mt-4 inline-block text-violet-600 dark:text-violet-400"
|
||||||
|
href="mailto:{content.email}">{content.email}</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mt-14 border-t border-gray-200 pt-8 dark:border-zinc-800">
|
<section
|
||||||
<h2 class="text-xl font-semibold tracking-tight">Twitch</h2>
|
class="mt-14 border-t border-gray-200 pt-8 dark:border-zinc-800"
|
||||||
{#if twitch?.configured && twitch?.found}
|
>
|
||||||
<div class="mt-6 grid grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-4">
|
<h2 class="text-xl font-semibold tracking-tight">Twitch</h2>
|
||||||
<div
|
{#if twitch?.configured && twitch?.found}
|
||||||
class="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-neutral-50 p-5 dark:border-zinc-800 dark:bg-neutral-900"
|
<div
|
||||||
>
|
class="mt-6 grid grid-cols-[repeat(auto-fit,minmax(160px,1fr))] gap-4"
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">Followers</span>
|
>
|
||||||
<span class="text-3xl font-semibold tracking-tight">{formatCount(twitch?.followers)}</span>
|
<div
|
||||||
</div>
|
class="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-neutral-50 p-5 dark:border-zinc-800 dark:bg-neutral-900"
|
||||||
</div>
|
>
|
||||||
{:else}
|
<span class="text-sm text-gray-500 dark:text-gray-400"
|
||||||
<p class="text-gray-500 dark:text-gray-400">{twitch?.message ?? 'Twitch stats are unavailable.'}</p>
|
>Followers</span
|
||||||
{/if}
|
>
|
||||||
</section>
|
<span class="text-3xl font-semibold tracking-tight"
|
||||||
|
>{formatCount(twitch?.followers)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
{twitch?.message ?? "Twitch stats are unavailable."}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mt-14 border-t border-gray-200 pt-8 dark:border-zinc-800">
|
<section
|
||||||
<h2 class="text-xl font-semibold tracking-tight">Platforms</h2>
|
class="mt-14 border-t border-gray-200 pt-8 dark:border-zinc-800"
|
||||||
<div class="mt-6 grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-4">
|
>
|
||||||
{#each platforms as platform (platform.name)}
|
<h2 class="text-xl font-semibold tracking-tight">Platforms</h2>
|
||||||
<a
|
<div
|
||||||
class="flex flex-col gap-1 rounded-2xl border border-gray-200 bg-neutral-50 p-5 transition-colors hover:border-violet-600 dark:border-zinc-800 dark:bg-neutral-900 dark:hover:border-violet-400"
|
class="mt-6 grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-4"
|
||||||
href={platform.url}
|
>
|
||||||
target="_blank"
|
{#each platforms as platform (platform.name)}
|
||||||
rel="noreferrer"
|
<a
|
||||||
>
|
class="flex flex-col gap-1 rounded-2xl border border-gray-200 bg-neutral-50 p-5 transition-colors hover:border-violet-600 dark:border-zinc-800 dark:bg-neutral-900 dark:hover:border-violet-400"
|
||||||
<span class="text-sm font-semibold tracking-[0.08em] uppercase text-gray-500 dark:text-gray-400">
|
href={platform.url}
|
||||||
{platform.name}
|
target="_blank"
|
||||||
</span>
|
rel="noreferrer"
|
||||||
<span class="text-base">{platform.handle}</span>
|
>
|
||||||
<span class="mt-2 text-2xl font-semibold tracking-tight">
|
<span
|
||||||
{formatCount(platform.followers)} followers
|
class="text-sm font-semibold tracking-[0.08em] uppercase text-gray-500 dark:text-gray-400"
|
||||||
</span>
|
>
|
||||||
</a>
|
{platform.name}
|
||||||
{/each}
|
</span>
|
||||||
</div>
|
<span class="text-base">{platform.handle}</span>
|
||||||
</section>
|
<span
|
||||||
</main>
|
class="mt-2 text-2xl font-semibold tracking-tight"
|
||||||
|
>
|
||||||
|
{formatCount(platform.followers)} followers
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue