mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-06-25 08:45:22 -04:00
Add card components, cards route, xml-bible tests, and progress page update
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import type { Attachment } from "svelte/attachments";
|
||||
|
||||
interface Props {
|
||||
front: string;
|
||||
back: string;
|
||||
}
|
||||
|
||||
let { front, back }: Props = $props();
|
||||
|
||||
let fanned = $state(false);
|
||||
|
||||
const cardDeck: Attachment<HTMLDivElement> = (node) => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
fanned = entry.isIntersecting;
|
||||
}
|
||||
},
|
||||
{ rootMargin: "-40% 0px -40% 0px", threshold: 0 }
|
||||
);
|
||||
|
||||
observer.observe(node);
|
||||
|
||||
const onEnter = () => (fanned = true);
|
||||
const onLeave = () => (fanned = false);
|
||||
|
||||
node.addEventListener("mouseenter", onEnter);
|
||||
node.addEventListener("mouseleave", onLeave);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
node.removeEventListener("mouseenter", onEnter);
|
||||
node.removeEventListener("mouseleave", onLeave);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
{@attach cardDeck}
|
||||
class="relative h-64 w-48 cursor-pointer"
|
||||
role="img"
|
||||
aria-label="Card deck"
|
||||
>
|
||||
<!-- Back card -->
|
||||
<img
|
||||
src={back}
|
||||
alt="Back"
|
||||
class="absolute inset-0 max-h-64 w-full object-contain drop-shadow-md transition-all duration-500 ease-in-out"
|
||||
style:transform={fanned ? "translateX(85px) rotate(5deg)" : "rotate(-2deg)"}
|
||||
style:z-index="1"
|
||||
/>
|
||||
<!-- Front card -->
|
||||
<img
|
||||
src={front}
|
||||
alt="Front"
|
||||
class="absolute inset-0 max-h-64 w-full object-contain drop-shadow-md transition-all duration-500 ease-in-out"
|
||||
style:transform={fanned ? "translateX(-85px) rotate(-5deg)" : "rotate(2deg)"}
|
||||
style:z-index="2"
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import FrontBack from "$lib/components/cards/FrontBack.svelte";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>BIBDLE Cards</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="text-center pb-72">Collectible Bible Verse Trading Cards</div>
|
||||
<div class="min-h-dvh py-10 px-4">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-center">
|
||||
<FrontBack
|
||||
front="/cards/Esther_4_front.png"
|
||||
back="/cards/Esther_4_back.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { SvelteDate } from "svelte/reactivity";
|
||||
import { onMount } from "svelte";
|
||||
import AuthModal from "$lib/components/AuthModal.svelte";
|
||||
import Container from "$lib/components/Container.svelte";
|
||||
@@ -98,6 +99,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isRecent(dateStr: string | null): boolean {
|
||||
if (!dateStr || !browser) return false;
|
||||
const sevenDaysAgo = new SvelteDate();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
const achievedDate = new Date(dateStr + "T00:00:00Z");
|
||||
return achievedDate >= sevenDaysAgo;
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const d = new Date(dateStr + "T00:00:00Z");
|
||||
return d.toLocaleDateString("en-US", {
|
||||
@@ -514,8 +523,15 @@
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-100 mb-3">🏆 Achievements</h2>
|
||||
<div class="grid grid-cols-3 md:grid-cols-4 gap-2 md:gap-3">
|
||||
{#each prog.milestones.filter(m => m.achieved) as milestone (milestone.id)}
|
||||
<Container class="p-3 min-h-[130px]">
|
||||
{#each prog.milestones
|
||||
.filter(m => m.achieved)
|
||||
.sort((a, b) => {
|
||||
if (!a.achievedDate && !b.achievedDate) return 0;
|
||||
if (!a.achievedDate) return 1;
|
||||
if (!b.achievedDate) return -1;
|
||||
return a.achievedDate.localeCompare(b.achievedDate);
|
||||
}) as milestone (milestone.id)}
|
||||
<Container class="p-3 min-h-[130px] {isRecent(milestone.achievedDate) ? 'recent-achievement' : ''}">
|
||||
<div class="text-center flex flex-col items-center justify-center h-full">
|
||||
<div class="text-2xl mb-1">{milestone.emoji}</div>
|
||||
<div class="text-sm font-bold text-yellow-300 leading-tight mb-1">
|
||||
@@ -625,3 +641,17 @@
|
||||
</div>
|
||||
|
||||
<AuthModal bind:isOpen={authModalOpen} {anonymousId} />
|
||||
|
||||
<style>
|
||||
@keyframes breathe-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 6px 2px rgba(251, 146, 60, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 18px 6px rgba(251, 146, 60, 0.65);
|
||||
}
|
||||
}
|
||||
:global(.recent-achievement) {
|
||||
animation: breathe-glow 2.5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user