Skip to content
Merged
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
11 changes: 9 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./globals.css"
import type { Metadata } from "next"
import { Inter, Crimson_Text } from "next/font/google"
import { Inter, Crimson_Text, Caveat } from "next/font/google"
import { Toaster } from "@/components/ui/toaster"
import { ThemeProvider } from "@/components/theme-provider"
import { ParchmentFilters } from "@/components/ui/parchment-filters"
Expand All @@ -16,6 +16,10 @@ const crimson = Crimson_Text({
subsets: ["latin"],
variable: "--font-serif",
})
const caveat = Caveat({
subsets: ["latin"],
variable: "--font-handwriting",
})

export const metadata: Metadata = {
title: "TaleTrail",
Expand Down Expand Up @@ -49,7 +53,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo

return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} ${crimson.variable} font-sans`}>
<body
className={`${inter.variable} ${crimson.variable} ${caveat.variable} font-sans`}
suppressHydrationWarning
>
<ParchmentFilters />
<ThemeProvider
attribute="class"
Expand Down
49 changes: 49 additions & 0 deletions app/showcase/book-instruction/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client"

import React, { useState } from "react"
import { BookStickerInstruction } from "@/components/book-sticker-instruction"
import { Button } from "@/components/ui/button"
import { RefreshCw } from "lucide-react"

export default function BookInstructionShowcasePage() {
const [code, setCode] = useState("ABCD-1234")
const [key, setKey] = useState(0)

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable key.

Copilot uses AI. Check for mistakes.

const regenerate = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let newCode = ""
for (let i = 0; i < 4; i++) newCode += chars.charAt(Math.floor(Math.random() * chars.length))
newCode += "-"
for (let i = 0; i < 4; i++) newCode += chars.charAt(Math.floor(Math.random() * chars.length))
setCode(newCode)
setKey((prev) => prev + 1)
}

return (
<div className="min-h-screen bg-stone-100 p-8 flex flex-col items-center justify-center space-y-12">
<div className="max-w-2xl text-center space-y-4">
<h1 className="text-4xl font-serif font-bold text-stone-800">
Tracking Instruction Showcase
</h1>
<p className="text-stone-600">Visualizing the "Inside Cover" instruction component.</p>
</div>

<div className="w-full max-w-md bg-white p-8 rounded-xl shadow-xl border border-stone-200">
<h2 className="text-xl font-bold text-center mb-6 text-stone-700">Component Demo</h2>

{/* The component under test */}
<BookStickerInstruction
code={code}
coverUrl="http://books.google.com/books/content?id=B1hSG45JCX4C&printsec=frontcover&img=1&zoom=1&source=gbs_api"

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded cover URL in the showcase may break if the Google Books API removes or changes this image. Consider using a locally hosted sample image or making this more robust by documenting that it's for demonstration purposes only.

Copilot uses AI. Check for mistakes.
/>

<div className="mt-8 flex justify-center">
<Button onClick={regenerate} variant="outline" className="gap-2">
<RefreshCw className="w-4 h-4" />
Generate New Code
</Button>
</div>
</div>
</div>
)
}
16 changes: 2 additions & 14 deletions components/add-sighting-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { GoogleBookSearch } from "@/components/google-book-search"
import { ParchmentFrame } from "@/components/ui/parchment-frame"
import { useToast } from "@/hooks/use-toast"
import useLocation from "@/hooks/use-location"
import { BookStickerInstruction } from "@/components/book-sticker-instruction"

export function AddSightingDrawer() {
const [open, setOpen] = useState(false)
Expand Down Expand Up @@ -235,20 +236,7 @@ export function AddSightingDrawer() {
</div>
) : (
<div className="space-y-6 text-center animate-in zoom-in-95 duration-500 py-2">
<div className="space-y-2">
<h2 className="font-serif text-xl font-bold text-primary">
Ready for Adventure!
</h2>
<p className="text-sm text-muted-foreground">
Write this code on the inside cover:
</p>
</div>

<div className="py-6 px-4 bg-white/50 rounded-lg border-2 border-dashed border-primary/20 backdrop-blur-sm">
<p className="font-mono text-3xl font-bold tracking-widest text-primary select-all">
{generatedCode}
</p>
</div>
<BookStickerInstruction code={generatedCode} coverUrl={selectedBook?.coverUrl} />

<Button variant="outline" onClick={resetState} className="mt-2">
Register another book
Expand Down
266 changes: 266 additions & 0 deletions components/book-sticker-instruction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
"use client"

import { useState, useEffect } from "react"
import { Check, Copy } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import AnimatePresence.

Suggested change
import { motion, AnimatePresence } from "framer-motion"
import { motion } from "framer-motion"

Copilot uses AI. Check for mistakes.
import confetti from "canvas-confetti"

import { Button } from "@/components/ui/button"
import { ParchmentFrame } from "@/components/ui/parchment-frame"
import { cn } from "@/lib/utils"

interface BookStickerInstructionProps {
code: string
coverUrl?: string
className?: string
}

const TypewriterText = ({
text,
delay = 0,
className,
}: {
text: string
delay?: number
className?: string
}) => {
// Split text into characters
const characters = text.split("")

const container = {
hidden: { opacity: 0 },
visible: (i = 1) => ({
opacity: 1,
transition: { staggerChildren: 0.1, delayChildren: delay },
}),
}

const child = {
visible: {
opacity: 1,
y: 0,
transition: {
type: "spring",
damping: 12,
stiffness: 100,
} as any, // Cast to any to bypass strict variant typing issues with spring

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The as any type assertions used here to bypass TypeScript's strict variant typing are not ideal. Consider using proper type definitions or defining a more specific type for the transition object. This pattern appears twice (lines 46 and 55) and can hide potential type errors.

Copilot uses AI. Check for mistakes.
},
hidden: {
opacity: 0,
y: 5,
transition: {
type: "spring",
damping: 12,
stiffness: 100,
} as any,
},
}

return (
<motion.div
style={{ display: "inline-block" }} // Ensure inline-block for proper spacing
variants={container}
initial="hidden"
animate="visible"
className={className}
>
{characters.map((char, index) => (
<motion.span variants={child} key={index}>
{char === " " ? "\u00A0" : char}
</motion.span>
))}
</motion.div>
)
}

export function BookStickerInstruction({ code, coverUrl, className }: BookStickerInstructionProps) {
const [copied, setCopied] = useState(false)
const [isOpen, setIsOpen] = useState(false)

useEffect(() => {
// Open the book shortly after mount
const timer = setTimeout(() => {
setIsOpen(true)
}, 500)

return () => clearTimeout(timer)
}, [])

useEffect(() => {
if (isOpen) {
// Trigger confetti when book opens and ink dries (approx 2s delay total relative to open)
// Let's time it with the code writing completion
const confettiTimer = setTimeout(() => {
const end = Date.now() + 1000
const colors = ["#a8e6cf", "#dcedc1", "#ffd3b6", "#ffaaa5", "#ff8b94"]

;(function frame() {
confetti({
particleCount: 2,
angle: 60,
spread: 55,
origin: { x: 0 },
colors: colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: { x: 1 },
colors: colors,
})

if (Date.now() < end) {
requestAnimationFrame(frame)
}
})()
}, 2500) // Delay to match writing animation

return () => clearTimeout(confettiTimer)
}
}, [isOpen])

const handleCopy = async () => {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}

const [isInsideVisible, setIsInsideVisible] = useState(false)

// Fallback cover if none provided
const finalCoverUrl = coverUrl || "/images/placeholder-cover.jpg"
Comment on lines +131 to +132

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback image URL /images/placeholder-cover.jpg may not exist. Consider either ensuring this asset exists in the public directory, or handling the case where no cover is provided more gracefully (e.g., showing a generic placeholder component).

Suggested change
// Fallback cover if none provided
const finalCoverUrl = coverUrl || "/images/placeholder-cover.jpg"
// Fallback cover if none provided: use an inline SVG data URL so it always exists
const placeholderCoverSvg =
'data:image/svg+xml;utf8,' +
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="600" viewBox="0 0 400 600">
<defs>
<linearGradient id="grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f5f5f4"/>
<stop offset="100%" stop-color="#e7e5e4"/>
</linearGradient>
</defs>
<rect width="400" height="600" fill="url(#grad)" rx="24" ry="24"/>
<rect x="36" y="60" width="328" height="36" fill="#a8a29e" opacity="0.35" rx="6"/>
<rect x="36" y="116" width="328" height="20" fill="#a8a29e" opacity="0.2" rx="4"/>
<rect x="36" y="146" width="260" height="20" fill="#a8a29e" opacity="0.2" rx="4"/>
<rect x="36" y="176" width="220" height="20" fill="#a8a29e" opacity="0.2" rx="4"/>
<rect x="36" y="230" width="328" height="260" fill="#d6d3d1" opacity="0.35" rx="12"/>
<text x="50%" y="520" text-anchor="middle" font-family="system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="20" fill="#78716c">
Book cover
</text>
</svg>`
)
const finalCoverUrl = coverUrl || placeholderCoverSvg

Copilot uses AI. Check for mistakes.

Comment on lines +131 to +133

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable finalCoverUrl.

Suggested change
// Fallback cover if none provided
const finalCoverUrl = coverUrl || "/images/placeholder-cover.jpg"

Copilot uses AI. Check for mistakes.
return (
<div className={cn("relative w-full max-w-md mx-auto p-4 perspective-[1200px]", className)}>
<div className="flex gap-2 items-center justify-center relative">
{/* The 3D Book Container */}
<div className="relative w-full aspect-[1.3] mt-4 mb-2 max-w-[300px]">
{/* The Book Itself */}
<motion.div
className="relative w-full h-full preserve-3d origin-left"
initial={false}
animate={{
rotateY: isOpen ? -10 : 0,
x: isOpen ? "100%" : "0%",
}}
transition={{ duration: 1.5, ease: "easeInOut" }}
>
{/* RIGHT PAGE (The "Book Block" that stays underneath) */}
<div className="absolute inset-0 w-full h-full bg-[#fdfbf6] rounded-r-md shadow-lg border-l border-stone-200">
{/* Binding Shadow (Left Inset) */}
<div className="absolute inset-0 pointer-events-none shadow-[inset_12px_0_15px_-4px_rgba(30,20,10,0.15)] z-10 rounded-r-md" />
{/* Page texture/lines */}
<div className="absolute inset-4 border-2 border-stone-100/50" />
<div className="absolute right-0 top-0 bottom-0 w-1 bg-stone-200/50" />{" "}
{/* Edge depth */}
</div>

{/* FRONT COVER ASSEMBLY (Flips Open) */}
<motion.div
className="absolute inset-0 w-full h-full origin-left preserve-3d z-20"
initial={{ rotateY: 0 }}
animate={{
rotateY: isOpen ? -180 : 0,
}}
transition={{ duration: 1.5, type: "spring", stiffness: 40, damping: 12 }}
onUpdate={(latest) => {
// Track rotation to toggle visibility state at 90 degrees (halfway)
if (typeof latest.rotateY === "number") {
const angle = latest.rotateY
if (angle < -90 && !isInsideVisible) setIsInsideVisible(true)
if (angle > -90 && isInsideVisible) setIsInsideVisible(false)
}
}}
>
{/* FRONT FACE (Outer Cover) */}
{!isInsideVisible && (
<div className="absolute inset-0 w-full h-full bg-amber-800 rounded-r-md shadow-xl overflow-hidden border-2 border-amber-900/20 backface-hidden">
{/* Spine effect */}
<div className="absolute left-0 top-0 bottom-0 w-4 bg-gradient-to-r from-black/20 to-transparent z-10" />

{/* Cover Image */}
{coverUrl ? (
<img
src={coverUrl}
alt="Book Cover"

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing alt text for the book cover image. The alt attribute should provide meaningful description of the book cover for accessibility purposes, not just "Book Cover". Consider using the book title or description if available.

Suggested change
alt="Book Cover"
alt="Illustrated book cover"

Copilot uses AI. Check for mistakes.
className="w-full h-full opacity-90 mix-blend-overlay"

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coverUrl is used directly in an img src without validation. Consider adding error handling for failed image loads (e.g., using the onError event) to prevent broken image displays and provide a better user experience.

Suggested change
className="w-full h-full opacity-90 mix-blend-overlay"
className="w-full h-full opacity-90 mix-blend-overlay"
onError={(e) => {
e.currentTarget.style.display = "none"
}}

Copilot uses AI. Check for mistakes.
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-amber-700 text-amber-100 p-6 text-center">
<span className="font-serif font-bold text-lg opacity-50">
TaleTrail Journey
</span>
</div>
)}

{/* Cover Texture Overlay */}
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/leather.png')] opacity-30 mix-blend-multiply" />

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using an external texture URL from transparenttextures.com creates a dependency on a third-party service. If this service goes down, the texture will fail to load. Consider hosting the texture locally or inlining it as a data URI for better reliability.

Suggested change
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/leather.png')] opacity-30 mix-blend-multiply" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(0,0,0,0.15),transparent_50%),radial-gradient(circle_at_80%_80%,rgba(0,0,0,0.2),transparent_55%)] opacity-30 mix-blend-multiply" />

Copilot uses AI. Check for mistakes.
</div>
)}

{/* BACK FACE (Inside Cover - Functional Area) */}
{isInsideVisible && (
<div
className="absolute inset-0 w-full h-full bg-[#f8f5e6] rounded-l-md overflow-hidden backface-hidden"
style={{ transform: "rotateY(180deg)" }}
>
{/* Binding Shadow (Right Inset) */}
<div className="absolute inset-0 pointer-events-none shadow-[inset_-12px_0_15px_-4px_rgba(30,20,10,0.15)] z-20 rounded-l-md" />

{/* Parchment/Instruction Content - Only fully render/animate when visible */}
<ParchmentFrame
variant="decorated"
className="h-full flex flex-col items-center justify-center text-center shadow-inner"
>
<div className="space-y-2 w-full relative z-10 scale-90">
{" "}
{/* Slight scale down to fit */}
<div className="space-y-2">
{/* Handwriting Animation for URL */}
<div className="h-4 flex items-center justify-center">
<TypewriterText
text="TaleTrail.org"
delay={0.2} // Reduced delay since we wait for the page flip now
className="font-handwriting font-bold text-amber-800 tracking-wide text-xl"
/>
</div>

{/* Handwriting Animation for Code */}
<div
className="relative group/code cursor-pointer flex justify-center mt-2"
onClick={handleCopy}
>
<div className="font-handwriting text-2xl font-bold text-stone-800 tracking-widest border-2 border-dashed border-stone-300 rounded px-3 py-1 bg-white/50 min-w-[160px] min-h-[48px] flex items-center justify-center">
<TypewriterText
text={code}
delay={1.2} // Relative to the URL finishing
/>
</div>

<div className="absolute -right-8 top-1/2 -translate-y-1/2 opacity-0 group-hover/code:opacity-100 transition-opacity">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-amber-700 hover:text-amber-900 hover:bg-amber-100/50"
>
{copied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
Comment on lines +241 to +253

Copilot AI Dec 28, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy button and code text are only accessible via hover (group-hover/code). This creates accessibility issues for keyboard-only users and touch device users who cannot hover. Consider making the copy button always visible or providing an alternative interaction method.

Copilot uses AI. Check for mistakes.
</div>
</div>
</div>
</ParchmentFrame>
</div>
)}
</motion.div>
</motion.div>
</div>
</div>
</div>
)
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,21 @@
"@radix-ui/react-tooltip": "^1.1.2",
"@supabase/auth-helpers-nextjs": "^0.9.0",
"@supabase/supabase-js": "^2.39.0",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"autoprefixer": "10.4.15",
"bufferutil": "^4.0.9",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"eslint": "8.49.0",
"eslint-config-next": "^16.1.0",
"framer-motion": "^12.23.26",
"input-otp": "^1.2.4",
"leaflet": "^1.9.4",
"lucide-react": "^0.446.0",
Expand Down
Binary file added public/images/heros/book-boy-write-code.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/leo-pen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading