mirror of
https://github.com/hanzo-apps/gallery.git
synced 2026-11-02 21:25:15 +00:00
265 lines
8.8 KiB
TypeScript
265 lines
8.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import Image from 'next/image';
|
|
import { YStack, XStack, H1, H2, H3, H4, Text, Button, Spinner } from '@hanzo/ui';
|
|
import { Grid } from '@hanzo/ui/grid';
|
|
import { templates } from './templates-data';
|
|
import { shot } from './lib/shot';
|
|
import { getUniqueTemplates } from './lib/template-utils';
|
|
import type { Template } from './templates-data';
|
|
import { c, t, at, clip, hue, tint } from './lib/design';
|
|
import { Stars } from './components/stars';
|
|
|
|
const pulse = { animation: 'pulse 2s cubic-bezier(.4,0,.6,1) infinite' };
|
|
|
|
/** A rounded chip: the framework, the category, the tier. */
|
|
function Chip({ children, ...rest }: { children: React.ReactNode; [k: string]: unknown }) {
|
|
return (
|
|
<Text
|
|
paddingHorizontal={12}
|
|
paddingVertical={4}
|
|
borderRadius="var(--radius-lg, 0.75rem)"
|
|
borderWidth={1}
|
|
{...t.sm}
|
|
fontWeight={500}
|
|
{...rest}
|
|
>
|
|
{children}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
export default function NotFound() {
|
|
const [pick, setPick] = useState<Template | null>(null);
|
|
const [fading, setFading] = useState(false);
|
|
const unique = getUniqueTemplates(templates);
|
|
|
|
const another = () => unique[Math.floor(Math.random() * unique.length)];
|
|
|
|
useEffect(() => {
|
|
setPick(another());
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
function reroll() {
|
|
setFading(true);
|
|
setTimeout(() => {
|
|
setPick(another());
|
|
setFading(false);
|
|
}, 200);
|
|
}
|
|
|
|
if (!pick) {
|
|
return (
|
|
<YStack minHeight="100vh" backgroundColor={c.ink} alignItems="center" justifyContent="center">
|
|
<Spinner size={64} color={c.blue500} />
|
|
</YStack>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<YStack
|
|
minHeight="100vh"
|
|
backgroundColor={c.ink}
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
padding={16}
|
|
>
|
|
<YStack position="absolute" top={0} left={0} right={0} bottom={0} overflow="hidden" pointerEvents="none">
|
|
<YStack
|
|
position="absolute"
|
|
top="25%"
|
|
left="25%"
|
|
width={384}
|
|
height={384}
|
|
borderRadius={9999}
|
|
backgroundColor={at(c.blue500, 0.1)}
|
|
filter="blur(120px)"
|
|
style={pulse}
|
|
/>
|
|
<YStack
|
|
position="absolute"
|
|
bottom="25%"
|
|
right="25%"
|
|
width={384}
|
|
height={384}
|
|
borderRadius={9999}
|
|
backgroundColor={at(c.purple500, 0.1)}
|
|
filter="blur(120px)"
|
|
style={{ animation: 'pulse 2s cubic-bezier(.4,0,.6,1) 1s infinite' }}
|
|
/>
|
|
</YStack>
|
|
|
|
<YStack position="relative" zIndex={10} width="100%" maxWidth={896}>
|
|
<YStack alignItems="center" marginBottom={48}>
|
|
<H1 {...t.xl9} fontWeight={700} marginBottom={16} style={pulse} {...clip(c.wash)}>
|
|
404
|
|
</H1>
|
|
<H2 {...t.xl3} fontWeight={700} color={c.neutral300} marginBottom={12} textAlign="center">
|
|
Oops! Template Not Found
|
|
</H2>
|
|
<Text {...t.lg} color={c.neutral500} marginBottom={32} textAlign="center">
|
|
The template you're looking for doesn't exist. But don't worry—we've got plenty
|
|
more!
|
|
</Text>
|
|
</YStack>
|
|
|
|
<YStack
|
|
transition="quick"
|
|
backgroundImage={`linear-gradient(to bottom right, ${at(c.neutral900, 0.5)}, ${at(c.neutral800, 0.5)})`}
|
|
backdropFilter="blur(24px)"
|
|
borderRadius="var(--radius-3xl, 1.5rem)"
|
|
borderWidth={1}
|
|
borderColor={c.white10}
|
|
padding={32}
|
|
boxShadow="0 25px 50px rgb(0 0 0 / .25)"
|
|
opacity={fading ? 0.5 : 1}
|
|
scale={fading ? 0.95 : 1}
|
|
>
|
|
<XStack alignItems="center" gap={12} marginBottom={24}>
|
|
<Text {...t.xl3}>✨</Text>
|
|
<H3 {...t.xl2} fontWeight={700} color="#fff">
|
|
How about this instead?
|
|
</H3>
|
|
</XStack>
|
|
|
|
<Grid columns={{ min: 280, max: 2 }} gap={24} style={{ marginBottom: 24 }}>
|
|
<YStack
|
|
position="relative"
|
|
aspectRatio={16 / 9}
|
|
borderRadius="var(--radius-xl, 1rem)"
|
|
overflow="hidden"
|
|
borderWidth={1}
|
|
borderColor={c.white10}
|
|
boxShadow="0 10px 15px rgb(0 0 0 / .1), 0 4px 6px rgb(0 0 0 / .05)"
|
|
>
|
|
<Image
|
|
src={shot(pick.screenshot)}
|
|
alt={pick.displayName}
|
|
fill
|
|
unoptimized
|
|
style={{ objectFit: 'cover' }}
|
|
/>
|
|
</YStack>
|
|
|
|
<YStack justifyContent="center">
|
|
<H4 {...t.xl3} fontWeight={700} marginBottom={12} {...clip(c.washShort)}>
|
|
{pick.displayName}
|
|
</H4>
|
|
<Text
|
|
color={c.neutral400}
|
|
marginBottom={16}
|
|
style={{
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 3,
|
|
WebkitBoxOrient: 'vertical',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{pick.description ||
|
|
`Premium ${pick.displayName} template with modern design and functionality.`}
|
|
</Text>
|
|
|
|
<XStack flexWrap="wrap" gap={8} marginBottom={16}>
|
|
<Chip {...tint('blue')}>{pick.framework}</Chip>
|
|
<Chip {...tint('purple')}>{pick.category}</Chip>
|
|
<Chip {...tint(hue(pick.tier))}>Tier {pick.tier}</Chip>
|
|
</XStack>
|
|
|
|
<XStack alignItems="center" gap={8}>
|
|
<Stars n={pick.rating} size={t.xl} />
|
|
<Text {...t.sm} color={c.neutral500}>
|
|
({pick.rating}/5)
|
|
</Text>
|
|
</XStack>
|
|
</YStack>
|
|
</Grid>
|
|
|
|
<XStack flexWrap="wrap" gap={12} justifyContent="center">
|
|
<Button
|
|
render={<Link href={`/templates/${pick.slug}`} />}
|
|
transition="quickest"
|
|
height="auto"
|
|
paddingHorizontal={32}
|
|
paddingVertical={16}
|
|
borderRadius="var(--radius-xl, 1rem)"
|
|
backgroundImage={c.cool}
|
|
boxShadow={`0 10px 15px ${at(c.blue500, 0.5)}`}
|
|
hoverStyle={{
|
|
backgroundImage: c.coolHover,
|
|
scale: 1.05,
|
|
boxShadow: `0 10px 15px ${at(c.blue500, 0.7)}`,
|
|
}}
|
|
>
|
|
<Text {...t.lg} fontWeight={700} color="#fff">
|
|
🚀 View This Template
|
|
</Text>
|
|
</Button>
|
|
<Button
|
|
onPress={reroll}
|
|
disabled={fading}
|
|
transition="quickest"
|
|
height="auto"
|
|
paddingHorizontal={32}
|
|
paddingVertical={16}
|
|
borderRadius="var(--radius-xl, 1rem)"
|
|
backgroundColor={c.white5}
|
|
borderWidth={1}
|
|
borderColor={c.white20}
|
|
hoverStyle={{ backgroundColor: 'rgba(255,255,255,0.1)', scale: 1.05 }}
|
|
disabledStyle={{ opacity: 0.5, cursor: 'not-allowed' }}
|
|
>
|
|
<Text {...t.lg} fontWeight={700} color="#fff">
|
|
🎲 Show Another Random
|
|
</Text>
|
|
</Button>
|
|
</XStack>
|
|
</YStack>
|
|
|
|
<XStack flexWrap="wrap" gap={16} justifyContent="center" marginTop={48}>
|
|
<Button
|
|
render={<Link href="/gallery" />}
|
|
transition="quickest"
|
|
height="auto"
|
|
paddingHorizontal={24}
|
|
paddingVertical={12}
|
|
borderRadius="var(--radius-xl, 1rem)"
|
|
backgroundColor={at(c.neutral800, 0.5)}
|
|
borderWidth={1}
|
|
borderColor={c.neutral700}
|
|
hoverStyle={{ backgroundColor: at(c.neutral700, 0.5), borderColor: c.neutral600 }}
|
|
>
|
|
<Text fontWeight={500} color={c.neutral300}>
|
|
← Browse All Templates
|
|
</Text>
|
|
</Button>
|
|
<Button
|
|
render={<Link href="/" />}
|
|
transition="quickest"
|
|
height="auto"
|
|
paddingHorizontal={24}
|
|
paddingVertical={12}
|
|
borderRadius="var(--radius-xl, 1rem)"
|
|
backgroundColor={at(c.neutral800, 0.5)}
|
|
borderWidth={1}
|
|
borderColor={c.neutral700}
|
|
hoverStyle={{ backgroundColor: at(c.neutral700, 0.5), borderColor: c.neutral600 }}
|
|
>
|
|
<Text fontWeight={500} color={c.neutral300}>
|
|
🏠 Go Home
|
|
</Text>
|
|
</Button>
|
|
</XStack>
|
|
|
|
<YStack alignItems="center" marginTop={48}>
|
|
<Text {...t.sm} color={c.neutral600}>
|
|
💡 Fun fact: We have {unique.length} amazing templates waiting for you!
|
|
</Text>
|
|
</YStack>
|
|
</YStack>
|
|
</YStack>
|
|
);
|
|
}
|