Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/lib/cards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import { GifCardDefinition } from './media/GIFCard';
import { PopfeedReviewsCardDefinition } from './media/PopfeedReviews';
import { TealFMPlaysCardDefinition } from './media/TealFMPlaysCard';
import { RockskyPlaysCardDefinition } from './media/RockskyPlaysCard';
import {
DerakkumaBestsCardDefinition,
DerakkumaCircleCardDefinition,
DerakkumaProfileCardDefinition,
DerakkumaRecentPlaysCardDefinition
} from './media/DerakkumaCards';
import { PhotoGalleryCardDefinition } from './media/PhotoGalleryCard';
import { StandardSiteDocumentListCardDefinition } from './content/StandardSiteDocumentListCard';
import { StatusphereCardDefinition } from './media/StatusphereCard';
Expand Down Expand Up @@ -95,6 +101,10 @@ export const AllCardDefinitions = [
PopfeedReviewsCardDefinition,
TealFMPlaysCardDefinition,
RockskyPlaysCardDefinition,
DerakkumaProfileCardDefinition,
DerakkumaCircleCardDefinition,
DerakkumaRecentPlaysCardDefinition,
DerakkumaBestsCardDefinition,
PhotoGalleryCardDefinition,
StandardSiteDocumentListCardDefinition,
StatusphereCardDefinition,
Expand Down
101 changes: 101 additions & 0 deletions src/lib/cards/media/DerakkumaCards/DerakkumaCircleCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script lang="ts">
import type { Item } from '$lib/types';
import { onMount } from 'svelte';
import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context';
import { CardDefinitionsByType } from '../..';
import { blobUrl, type DerakkumaCircleValue, type RepoRecord } from './shared';

let { item }: { item: Item } = $props();
const data = getAdditionalUserData();
// svelte-ignore state_referenced_locally
let circle = $state(data[item.cardType] as RepoRecord<DerakkumaCircleValue> | undefined);
let did = getDidContext();
let handle = getHandleContext();

onMount(async () => {
if (circle) return;
circle = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { did, handle })) as
| RepoRecord<DerakkumaCircleValue>
| undefined;
data[item.cardType] = circle;
});

let value = $derived(circle?.value);
let characterImage = $derived(blobUrl(did, value?.characterImage));
let backgroundImage = $derived(blobUrl(did, value?.backgroundImage));
</script>

<div class="relative h-full w-full overflow-hidden">
{#if value}
{#if backgroundImage}
<img
src={backgroundImage}
alt=""
class="absolute inset-0 h-full w-full object-cover opacity-30"
/>
<div
class="accent:from-accent-300/80 accent:via-accent-300/60 accent:to-accent-500/30 absolute inset-0 bg-gradient-to-br from-white/80 via-white/70 to-white/30 dark:from-black/80 dark:via-black/70 dark:to-black/30"
></div>
{/if}

<div class="relative flex h-full gap-3 p-4">
<div class="flex min-w-0 flex-1 flex-col">
<div class="text-accent-500 accent:text-accent-950 truncate text-lg font-black">
{value.name || 'Derakkuma Circle'}
</div>

<div class="mt-1 flex flex-wrap gap-1.5 text-xs font-semibold">
{#if value.rank}
<span
class="accent:bg-accent-950/15 rounded-full bg-black/10 px-2 py-0.5 dark:bg-white/10"
>
Rank {value.rank}
</span>
{/if}
{#if value.totalPoints !== undefined}
<span
class="accent:bg-accent-950/15 rounded-full bg-black/10 px-2 py-0.5 dark:bg-white/10"
>
{value.totalPoints} pts
</span>
{/if}
</div>

{#if value.comment}
<div
class="text-base-600 dark:text-base-300 accent:text-accent-950/80 mt-2 line-clamp-2 text-sm"
>
{value.comment}
</div>
{/if}

<div
class="text-base-500 dark:text-base-400 accent:text-accent-950/70 mt-auto flex flex-col gap-0.5 text-xs"
>
{#if value.ownerName}
<div class="truncate">Owner {value.ownerName}</div>
{/if}
{#if value.circleCode}
<div class="truncate">Circle code {value.circleCode}</div>
{/if}
{#if value.daysUntilReset && value.daysUntilReset > 0}
<div>{value.daysUntilReset} days left</div>
{/if}
{#if value.nextRewardPoints && value.nextRewardPoints > 0}
<div>{value.nextRewardPoints} pts to next reward</div>
{/if}
</div>
</div>

{#if characterImage}
<img src={characterImage} alt="" class="h-full max-h-36 w-24 shrink-0 object-contain" />
{/if}
</div>
{:else}
<div
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center p-4 text-center text-sm"
>
Loading Derakkuma circle...
</div>
{/if}
</div>
106 changes: 106 additions & 0 deletions src/lib/cards/media/DerakkumaCards/DerakkumaProfileCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<script lang="ts">
import type { Item } from '$lib/types';
import { onMount } from 'svelte';
import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context';
import { CardDefinitionsByType } from '../..';
import { blobUrl, type DerakkumaProfileValue, type RepoRecord } from './shared';

let { item }: { item: Item } = $props();
const data = getAdditionalUserData();
// svelte-ignore state_referenced_locally
let profile = $state(data[item.cardType] as RepoRecord<DerakkumaProfileValue> | undefined);
let did = getDidContext();
let handle = getHandleContext();

onMount(async () => {
if (profile) return;
profile = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { did, handle })) as
| RepoRecord<DerakkumaProfileValue>
| undefined;
data[item.cardType] = profile;
});

let value = $derived(profile?.value);
let profileImage = $derived(blobUrl(did, value?.profileImage));
let ratingPlate = $derived(blobUrl(did, value?.ratingPlateImage));
let trophyPlate = $derived(blobUrl(did, value?.trophyPlateImage));
let partner = $derived(blobUrl(did, value?.partnerImage));
let course = $derived(blobUrl(did, value?.courseImage));
let classImage = $derived(blobUrl(did, value?.classImage));
let profileImageHasError = $state(false);
</script>

<div class="flex h-full w-full flex-col gap-3 overflow-hidden p-4">
{#if value}
<div class="flex min-h-0 flex-1 gap-3">
<div class="size-20 shrink-0">
{#if profileImage && !profileImageHasError}
<img
src={profileImage}
alt={value.playerName ?? ''}
class="h-full w-full rounded-xl object-cover"
onerror={() => {
profileImageHasError = true;
}}
/>
{:else}
<div
class="bg-base-200 dark:bg-base-800 accent:bg-accent-700/40 flex h-full w-full items-center justify-center rounded-xl text-lg"
>
🐻
</div>
{/if}
</div>
<div class="min-w-0 flex-1">
<div class="text-accent-500 accent:text-accent-950 truncate text-lg font-bold">
{value.playerName || 'Derakkuma'}
</div>
{#if value.friendCode}
<div class="text-base-500 dark:text-base-400 accent:text-accent-950/70 truncate text-xs">
Friend code {value.friendCode}
</div>
{/if}
<div class="mt-2 flex flex-col gap-1.5">
{#if trophyPlate}
<div class="relative h-7 overflow-hidden rounded-md">
<img src={trophyPlate} alt="" class="absolute inset-0 h-full w-full object-fill" />
<div
class="relative flex h-full items-center justify-center px-2 text-xs font-semibold text-white [text-shadow:0_1px_4px_rgba(0,0,0,.95)]"
>
{value.title}
</div>
</div>
{/if}
<div class="flex items-center gap-2">
{#if ratingPlate}
<div class="relative h-8 w-24 overflow-hidden rounded-md">
<img src={ratingPlate} alt="" class="absolute inset-0 h-full w-full object-fill" />
<div
class="relative flex h-full items-center justify-end pr-2 text-sm font-black text-white [text-shadow:0_1px_4px_rgba(0,0,0,.95)]"
>
{value.rating ?? 0}
</div>
</div>
{/if}
{#if course}<img src={course} alt="" class="h-6 w-11 object-contain" />{/if}
{#if value.stars}<span class="text-sm font-semibold">⭐×{value.stars}</span>{/if}
{#if classImage}<img src={classImage} alt="" class="h-6 w-11 object-contain" />{/if}
</div>
</div>
</div>
{#if partner}
<img
src={partner}
alt=""
class="hidden h-full max-h-28 w-20 shrink-0 object-contain @lg:block"
/>
{/if}
</div>
{:else}
<div
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm"
>
Loading Derakkuma profile...
</div>
{/if}
</div>
83 changes: 83 additions & 0 deletions src/lib/cards/media/DerakkumaCards/DerakkumaScoresCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts">
import type { Item } from '$lib/types';
import { onMount } from 'svelte';
import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context';
import { CardDefinitionsByType } from '../..';
import { RelativeTime } from '@foxui/time';
import { scoreMeta, scoreSubtitle, scoreTitle, type EnrichedDerakkumaScore } from './shared';

let { item }: { item: Item } = $props();
const data = getAdditionalUserData();
// svelte-ignore state_referenced_locally
let feed = $state(data[item.cardType] as EnrichedDerakkumaScore[] | undefined);
let did = getDidContext();
let handle = getHandleContext();

onMount(async () => {
if (feed) return;
feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { did, handle })) as
| EnrichedDerakkumaScore[]
| undefined;
data[item.cardType] = feed;
});

function dateFor(score: EnrichedDerakkumaScore): Date | undefined {
const value = score.value.playedAt ?? score.value.updatedAt ?? score.value.createdAt;
if (!value) return;
return new Date(value);
}
</script>

{#snippet fallbackArt()}
<div
class="bg-base-200 dark:bg-base-800 accent:bg-accent-700/40 flex size-11 items-center justify-center rounded-lg text-sm"
>
</div>
{/snippet}

<div class="z-10 flex h-full w-full flex-col gap-3 overflow-y-scroll p-4">
{#if feed && feed.length > 0}
{#each feed as score (score.uri)}
<div class="flex w-full items-center gap-3">
<div class="size-11 shrink-0">
{#if score.coverArtUrl}
<img src={score.coverArtUrl} alt="" class="size-11 rounded-lg object-cover" />
{:else}
{@render fallbackArt()}
{/if}
</div>
<div class="min-w-0 flex-1">
<div class="inline-flex w-full max-w-full justify-between gap-2">
<div
class="text-accent-500 accent:text-accent-950 min-w-0 flex-1 truncate font-semibold"
>
{scoreTitle(score)}
</div>
{#if dateFor(score)}
<div class="shrink-0 text-xs">
<RelativeTime date={dateFor(score)!} locale="en-US" /> ago
</div>
{/if}
</div>
<div class="truncate text-xs font-medium">{scoreSubtitle(score)}</div>
<div class="text-base-500 dark:text-base-400 accent:text-accent-950/70 truncate text-xs">
{scoreMeta(score)}
</div>
</div>
</div>
{/each}
{:else if feed}
<div
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm"
>
No Derakkuma records found.
</div>
{:else}
<div
class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm"
>
Loading Derakkuma records...
</div>
{/if}
</div>
Loading