@meycult/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +45 -0
- package/package.json +72 -0
- package/src/auth/Gate.tsx +14 -0
- package/src/auth/types.ts +12 -0
- package/src/brand/BrandName.tsx +12 -0
- package/src/brand/CornerGlow.tsx +14 -0
- package/src/brand/CultEye.tsx +14 -0
- package/src/brand/GodEmperor.tsx +8 -0
- package/src/brand/LaunchButton.tsx +27 -0
- package/src/brand/Logo.tsx +24 -0
- package/src/brand/Sigil.tsx +28 -0
- package/src/brand/TokenIcons.tsx +48 -0
- package/src/brand.ts +7 -0
- package/src/cn.ts +6 -0
- package/src/components/CultCountdown.tsx +119 -0
- package/src/fonts/NewRocker-Regular.woff2 +0 -0
- package/src/fonts/NotoSansEgyptianHieroglyphs-Regular.woff2 +0 -0
- package/src/index.ts +61 -0
- package/src/lib/images.ts +10 -0
- package/src/lib/money.ts +12 -0
- package/src/lib/store.ts +19 -0
- package/src/shell/types.ts +13 -0
- package/src/styles/theme.css +396 -0
- package/src/supabase/browser.ts +103 -0
- package/src/supabase/callback.ts +15 -0
- package/src/supabase/proxy.ts +36 -0
- package/src/supabase/server.ts +31 -0
- package/src/tokens.ts +37 -0
- package/src/ui/Callout.tsx +33 -0
- package/src/ui/animated-shiny-text.tsx +42 -0
- package/src/ui/card.tsx +102 -0
- package/src/ui/shimmer-button.tsx +81 -0
- package/src/unveil/LaunchStage.tsx +32 -0
- package/src/unveil/Tablet.tsx +23 -0
- package/src/unveil/bg.ts +59 -0
- package/src/unveil/bgStyles.ts +13 -0
- package/src/unveil/blocks.tsx +199 -0
- package/src/unveil/effects.tsx +200 -0
- package/src/unveil.ts +78 -0
package/src/ui/card.tsx
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../cn"
|
|
3
|
+
|
|
4
|
+
function Card({
|
|
5
|
+
className,
|
|
6
|
+
size = "default",
|
|
7
|
+
...props
|
|
8
|
+
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
data-slot="card"
|
|
12
|
+
data-size={size}
|
|
13
|
+
className={cn(
|
|
14
|
+
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
data-slot="card-header"
|
|
26
|
+
className={cn(
|
|
27
|
+
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-slot="card-title"
|
|
39
|
+
className={cn(
|
|
40
|
+
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
data-slot="card-description"
|
|
52
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="card-action"
|
|
62
|
+
className={cn(
|
|
63
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
data-slot="card-content"
|
|
75
|
+
className={cn("px-(--card-spacing)", className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-slot="card-footer"
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
Card,
|
|
96
|
+
CardHeader,
|
|
97
|
+
CardFooter,
|
|
98
|
+
CardTitle,
|
|
99
|
+
CardAction,
|
|
100
|
+
CardDescription,
|
|
101
|
+
CardContent,
|
|
102
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { type ComponentPropsWithoutRef, type CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../cn";
|
|
4
|
+
|
|
5
|
+
export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> {
|
|
6
|
+
shimmerColor?: string;
|
|
7
|
+
shimmerSize?: string;
|
|
8
|
+
borderRadius?: string;
|
|
9
|
+
shimmerDuration?: string;
|
|
10
|
+
background?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ShimmerButton = React.forwardRef<HTMLButtonElement, ShimmerButtonProps>(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
shimmerColor = "#b6ff7a",
|
|
19
|
+
shimmerSize = "0.05em",
|
|
20
|
+
shimmerDuration = "3s",
|
|
21
|
+
borderRadius = "100px",
|
|
22
|
+
background = "rgba(4, 16, 7, 1)",
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
...props
|
|
26
|
+
},
|
|
27
|
+
ref,
|
|
28
|
+
) => {
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
style={
|
|
32
|
+
{
|
|
33
|
+
"--spread": "90deg",
|
|
34
|
+
"--shimmer-color": shimmerColor,
|
|
35
|
+
"--radius": borderRadius,
|
|
36
|
+
"--speed": shimmerDuration,
|
|
37
|
+
"--cut": shimmerSize,
|
|
38
|
+
"--bg": background,
|
|
39
|
+
} as CSSProperties
|
|
40
|
+
}
|
|
41
|
+
className={cn(
|
|
42
|
+
"group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden [border-radius:var(--radius)] border border-white/10 px-6 py-3 whitespace-nowrap text-white [background:var(--bg)]",
|
|
43
|
+
"transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
ref={ref}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
className={cn(
|
|
51
|
+
"-z-30 blur-[2px]",
|
|
52
|
+
"[container-type:size] absolute inset-0 overflow-visible",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<div className="animate-shimmer-slide absolute inset-0 [aspect-ratio:1] h-[100cqh] [border-radius:0] [mask:none]">
|
|
56
|
+
<div className="animate-spin-around absolute -inset-full w-auto [translate:0_0] rotate-0 [background:conic-gradient(from_calc(270deg-(var(--spread)*0.5)),transparent_0,var(--shimmer-color)_var(--spread),transparent_var(--spread))]" />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
{children}
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
"absolute inset-0 size-full",
|
|
64
|
+
"rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]",
|
|
65
|
+
"transform-gpu transition-all duration-300 ease-in-out",
|
|
66
|
+
"group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]",
|
|
67
|
+
"group-active:shadow-[inset_0_-10px_10px_#ffffff3f]",
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
"absolute [inset:var(--cut)] -z-20 [border-radius:var(--radius)] [background:var(--bg)]",
|
|
74
|
+
)}
|
|
75
|
+
/>
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
ShimmerButton.displayName = "ShimmerButton";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/** Fixed ambient stage behind the Unveil countdown. Kept set only. */
|
|
4
|
+
import { useEffect } from 'react'
|
|
5
|
+
import { GrainVeil } from '@meyvoid/core/backgrounds'
|
|
6
|
+
import { BG_STYLES } from './bgStyles'
|
|
7
|
+
import { parseBgQuery, useUnveilBg } from './bg'
|
|
8
|
+
import { AshCanvas, BreathVignette, RuneCanvas, VeilFog } from './effects'
|
|
9
|
+
|
|
10
|
+
export function LaunchStage() {
|
|
11
|
+
const { effects, density, speed, opacity, grain, setMany } = useUnveilBg()
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
useUnveilBg.persist.rehydrate()
|
|
15
|
+
const next = parseBgQuery(window.location.search)
|
|
16
|
+
if (next) setMany(next)
|
|
17
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
const fx = { density, speed, opacity }
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true">
|
|
24
|
+
<style>{BG_STYLES}</style>
|
|
25
|
+
{effects.includes('veil') && <VeilFog opacity={opacity} />}
|
|
26
|
+
{effects.includes('ash') && <AshCanvas {...fx} />}
|
|
27
|
+
{effects.includes('runes') && <RuneCanvas {...fx} />}
|
|
28
|
+
{effects.includes('breath') && <BreathVignette />}
|
|
29
|
+
{grain && <GrainVeil />}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TabletPanel — yellow engraved tablet enclosing Unveil details.
|
|
3
|
+
* Dark bronze body, gold border, engraved hieroglyph frieze top + bottom
|
|
4
|
+
* (letterpress technique: darker-than-surface glyphs, light up-left edge).
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
|
|
8
|
+
const FRIEZE =
|
|
9
|
+
'𓂀 𓆣 𓅓 𓇳 𓊵 𓆙 𓆄 ☥ △ ◉ 𓂀 𓆣 𓅓 𓇳 𓊵 𓆙 𓆄 ☥ △ ◉ 𓂀 𓆣 𓅓 𓇳 𓊵 𓆙 𓆄 ☥ △ ◉'
|
|
10
|
+
|
|
11
|
+
export function TabletPanel({ children }: { children: ReactNode }) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="tablet-panel">
|
|
14
|
+
<div className="tablet-frieze tablet-frieze--top" aria-hidden="true">
|
|
15
|
+
{FRIEZE}
|
|
16
|
+
</div>
|
|
17
|
+
{children}
|
|
18
|
+
<div className="tablet-frieze tablet-frieze--bottom" aria-hidden="true">
|
|
19
|
+
{FRIEZE}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
package/src/unveil/bg.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeyCult Unveil background state. Kept set: glyphs, veil, ash, breath.
|
|
3
|
+
* The six retired layers live in @meyvoid/core backgrounds for reuse.
|
|
4
|
+
*/
|
|
5
|
+
import { createPersistStore } from '../lib/store'
|
|
6
|
+
|
|
7
|
+
export type BgEffect = 'runes' | 'veil' | 'ash' | 'breath'
|
|
8
|
+
|
|
9
|
+
export const BG_EFFECTS: Array<{ id: BgEffect; label: string }> = [
|
|
10
|
+
{ id: 'runes', label: 'Egyptian Glyphs' },
|
|
11
|
+
{ id: 'veil', label: 'Veil Fog' },
|
|
12
|
+
{ id: 'ash', label: 'Ash Flow-Field' },
|
|
13
|
+
{ id: 'breath', label: 'Breath Vignette' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export const BG_DEFAULTS: BgEffect[] = ['runes', 'veil', 'ash', 'breath']
|
|
17
|
+
|
|
18
|
+
interface BgState {
|
|
19
|
+
effects: BgEffect[]
|
|
20
|
+
density: number // 0-100
|
|
21
|
+
speed: number // 0-2
|
|
22
|
+
opacity: number // 0-1
|
|
23
|
+
grain: boolean
|
|
24
|
+
toggle: (e: BgEffect) => void
|
|
25
|
+
setMany: (e: BgEffect[]) => void
|
|
26
|
+
set: (p: Partial<Pick<BgState, 'density' | 'speed' | 'opacity' | 'grain'>>) => void
|
|
27
|
+
reset: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const useUnveilBg = createPersistStore<BgState>(
|
|
31
|
+
'meycult-unveil-bg-v1',
|
|
32
|
+
1,
|
|
33
|
+
(set) => ({
|
|
34
|
+
effects: BG_DEFAULTS,
|
|
35
|
+
density: 50,
|
|
36
|
+
speed: 0.6,
|
|
37
|
+
opacity: 0.8,
|
|
38
|
+
grain: true,
|
|
39
|
+
toggle: (e) =>
|
|
40
|
+
set((s) => ({
|
|
41
|
+
effects: s.effects.includes(e)
|
|
42
|
+
? s.effects.filter((x) => x !== e)
|
|
43
|
+
: [...s.effects, e],
|
|
44
|
+
})),
|
|
45
|
+
setMany: (effects) => set({ effects }),
|
|
46
|
+
set: (p) => set(p),
|
|
47
|
+
reset: () =>
|
|
48
|
+
set({ effects: BG_DEFAULTS, density: 50, speed: 0.6, opacity: 0.8, grain: true }),
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
/** Read-only ?bg=veil,runes share param (applied once on mount). */
|
|
53
|
+
export function parseBgQuery(search: string): BgEffect[] | null {
|
|
54
|
+
const m = new URLSearchParams(search).get('bg')
|
|
55
|
+
if (!m) return null
|
|
56
|
+
const ids = m.split(',').map((s) => s.trim()) as BgEffect[]
|
|
57
|
+
const valid = ids.filter((id) => BG_EFFECTS.some((e) => e.id === id))
|
|
58
|
+
return valid.length ? valid : null
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** CSS-only Unveil background layers (injected via <style> by LaunchStage).
|
|
2
|
+
* Kept set: glyphs canvas, veil fog, ash flow, breath vignette. The six
|
|
3
|
+
* retired layers live in @meyvoid/core backgrounds for future reuse. */
|
|
4
|
+
export const BG_STYLES = `
|
|
5
|
+
.mey-veil{position:absolute;inset:auto 0 0 0;height:44vh;filter:blur(60px);pointer-events:none}
|
|
6
|
+
.mey-veil i{position:absolute;width:55vw;height:22vh;border-radius:50%;background:radial-gradient(closest-side,rgba(255,230,0,.16),transparent 70%)}
|
|
7
|
+
.mey-veil i:nth-child(2){background:radial-gradient(closest-side,rgba(255,230,0,.18),transparent 70%);animation:mey-veil-drift 38s ease-in-out infinite alternate}
|
|
8
|
+
@keyframes mey-veil-drift{from{transform:translateX(-8vw)}to{transform:translateX(8vw)}}
|
|
9
|
+
.mey-breath{position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 180px 60px rgba(0,0,0,.75);animation:mey-breath 7s ease-in-out infinite}
|
|
10
|
+
@keyframes mey-breath{50%{box-shadow:inset 0 0 240px 90px rgba(0,0,0,.9)}}
|
|
11
|
+
.mey-bg-canvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
|
|
12
|
+
@media (prefers-reduced-motion:reduce){.mey-veil i:nth-child(2),.mey-breath{animation:none}}
|
|
13
|
+
`
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unveil page blocks — MeyGod festival anatomy in cult tokens:
|
|
5
|
+
* hero (eye + countdown), where/when (map), cards, hype+QR, notify.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import { MeyQR } from '@meyvoid/core/qr'
|
|
9
|
+
import CultEye from '../brand/CultEye'
|
|
10
|
+
import { CultCountdown } from '../components/CultCountdown'
|
|
11
|
+
import { UNVEIL_AT, unveilDayLabel, unveilLocalLabel } from '../unveil'
|
|
12
|
+
|
|
13
|
+
export function UnveilHero() {
|
|
14
|
+
return (
|
|
15
|
+
<section className="section section--hero">
|
|
16
|
+
<a href="/unveil/countdown" aria-label="Open the full-page countdown" data-testid="unveil-sigil">
|
|
17
|
+
<div className="w-[200px] sm:w-[280px]">
|
|
18
|
+
<CultEye fluid />
|
|
19
|
+
</div>
|
|
20
|
+
</a>
|
|
21
|
+
|
|
22
|
+
<div className="mt-2 flex flex-col items-center leading-none">
|
|
23
|
+
<span className="text-6xl sm:text-7xl font-bold tracking-wider">
|
|
24
|
+
<span className="brand-mey">Mey</span>
|
|
25
|
+
<span className="brand-cult">Cult</span>
|
|
26
|
+
</span>
|
|
27
|
+
<span className="brand-unveil unveil-glow mt-1 text-5xl sm:text-6xl">Unveil</span>
|
|
28
|
+
</div>
|
|
29
|
+
<p className="mt-3 text-sm text-text-muted tracking-wide">
|
|
30
|
+
{unveilDayLabel(UNVEIL_AT)} SAST · Stellenbosch
|
|
31
|
+
</p>
|
|
32
|
+
<p className="text-xs text-text-muted">{unveilLocalLabel(UNVEIL_AT)} your time</p>
|
|
33
|
+
|
|
34
|
+
<div className="mt-8 w-full max-w-2xl">
|
|
35
|
+
<CultCountdown />
|
|
36
|
+
</div>
|
|
37
|
+
<p className="mt-3 text-xs text-text-muted">Touch the sigil for the full-page countdown.</p>
|
|
38
|
+
|
|
39
|
+
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
|
40
|
+
<a
|
|
41
|
+
className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
|
|
42
|
+
href="/#crowdfunding"
|
|
43
|
+
>
|
|
44
|
+
Notify me
|
|
45
|
+
</a>
|
|
46
|
+
<a
|
|
47
|
+
className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text"
|
|
48
|
+
href="/unveil/countdown"
|
|
49
|
+
>
|
|
50
|
+
Full countdown
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WhereWhenProps {
|
|
58
|
+
venue?: string
|
|
59
|
+
city?: string
|
|
60
|
+
mapsUrl?: string
|
|
61
|
+
footnote?: string
|
|
62
|
+
map?: { src: string; alt: string }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function WhereWhen({
|
|
66
|
+
venue = 'Venue TBA — Stellenbosch',
|
|
67
|
+
city = 'Stellenbosch, Western Cape',
|
|
68
|
+
mapsUrl,
|
|
69
|
+
footnote = 'Doors 19:30. Free to attend. Bring a friend; leave as Cult.',
|
|
70
|
+
map,
|
|
71
|
+
}: WhereWhenProps) {
|
|
72
|
+
const href =
|
|
73
|
+
mapsUrl ?? 'https://www.google.com/maps/search/?api=1&query=-33.9347,18.8607'
|
|
74
|
+
return (
|
|
75
|
+
<div className="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-8 sm:gap-12 items-center">
|
|
76
|
+
<div className="justify-self-start w-full">
|
|
77
|
+
{map ? (
|
|
78
|
+
<a href={href} target="_blank" rel="noreferrer" className="block w-full group">
|
|
79
|
+
<div
|
|
80
|
+
className="w-full aspect-[2/1] overflow-hidden rounded-xl transition-transform duration-300 group-hover:scale-[1.02]"
|
|
81
|
+
style={{ border: '1px solid rgba(255,230,0,0.18)', boxShadow: '0 0 40px rgba(255,230,0,0.12)' }}
|
|
82
|
+
>
|
|
83
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
84
|
+
<img src={map.src} alt={map.alt} className="w-full h-full object-cover" />
|
|
85
|
+
</div>
|
|
86
|
+
<p className="mt-2 text-[10px] uppercase tracking-[0.25em] text-text-muted/60 text-center">
|
|
87
|
+
© OpenStreetMap contributors
|
|
88
|
+
</p>
|
|
89
|
+
</a>
|
|
90
|
+
) : (
|
|
91
|
+
<div
|
|
92
|
+
className="w-full aspect-[2/1] rounded-xl flex items-center justify-center"
|
|
93
|
+
style={{ border: '1px solid rgba(255,230,0,0.18)', background: 'rgba(0,0,0,0.3)' }}
|
|
94
|
+
>
|
|
95
|
+
<div className="w-[64px]">
|
|
96
|
+
<CultEye fluid />
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
<div className="text-center sm:text-right sm:border-l sm:border-dashed sm:pl-12 justify-self-end w-full border-line">
|
|
102
|
+
<h2 className="section-title" style={{ fontSize: '1.875rem', letterSpacing: '0.08em' }}>
|
|
103
|
+
Where and When
|
|
104
|
+
</h2>
|
|
105
|
+
<div className="mt-4 text-sm sm:text-base tracking-[0.2em] text-accent" style={{ fontFamily: 'var(--font-heading)' }}>
|
|
106
|
+
{unveilDayLabel(UNVEIL_AT).toUpperCase()}
|
|
107
|
+
</div>
|
|
108
|
+
<div className="mt-1 text-base sm:text-lg text-text font-medium">{venue}</div>
|
|
109
|
+
<div className="text-sm text-text-muted">{city}</div>
|
|
110
|
+
<div className="mt-4 text-[10px] uppercase tracking-[0.35em] text-text-muted">The Hour</div>
|
|
111
|
+
<div className="mt-0.5 text-4xl sm:text-5xl font-bold leading-none text-text" style={{ fontFamily: 'var(--font-heading)' }}>
|
|
112
|
+
20:00
|
|
113
|
+
</div>
|
|
114
|
+
{footnote && <p className="mt-4 text-xs sm:text-sm text-text-muted italic leading-relaxed">{footnote}</p>}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface UnveilCard {
|
|
121
|
+
title: ReactNode
|
|
122
|
+
body: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function CardGrid({ cards, cols = 3 }: { cards: UnveilCard[]; cols?: 2 | 3 }) {
|
|
126
|
+
const colCls = cols === 2 ? 'md:grid-cols-2' : 'md:grid-cols-2 lg:grid-cols-3'
|
|
127
|
+
return (
|
|
128
|
+
<div className={`grid grid-cols-1 ${colCls} gap-3 max-w-3xl mx-auto`}>
|
|
129
|
+
{cards.map((c, i) => (
|
|
130
|
+
<div key={i} className="landing-card p-4 text-center">
|
|
131
|
+
<h3 className="text-sm font-bold text-text uppercase tracking-wider" style={{ fontFamily: 'var(--font-heading)' }}>
|
|
132
|
+
{c.title}
|
|
133
|
+
</h3>
|
|
134
|
+
<p className="text-[11px] text-text-muted mt-1 leading-relaxed">{c.body}</p>
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function HypeRow({ qrUrl }: { qrUrl: string }) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="grid grid-cols-1 sm:grid-cols-[2fr_1fr] gap-8 items-center max-w-3xl mx-auto">
|
|
144
|
+
<div>
|
|
145
|
+
<h2 className="section-title">What is the Unveil?</h2>
|
|
146
|
+
<p className="text-text-muted text-sm sm:text-base leading-relaxed">
|
|
147
|
+
The first <span className="text-text font-semibold">playable prophecy market</span>. Not
|
|
148
|
+
watching — <span className="text-text font-semibold">playing</span>. Quests seeded hourly,
|
|
149
|
+
dual tokens, heroes that level as you wager. On stage in Stellenbosch, live on YouTube —
|
|
150
|
+
then in your pocket.
|
|
151
|
+
</p>
|
|
152
|
+
<p className="text-text-muted text-xs sm:text-sm leading-relaxed mt-3">
|
|
153
|
+
Free to play. Real USDC cash-out. Sweepstakes-legal.
|
|
154
|
+
</p>
|
|
155
|
+
<div className="mt-4 flex flex-wrap gap-3">
|
|
156
|
+
<a className="px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider" href="/#crowdfunding">
|
|
157
|
+
Get notified
|
|
158
|
+
</a>
|
|
159
|
+
<a className="px-6 py-2.5 rounded-lg border border-line/40 text-sm uppercase tracking-wider text-text" href="/whitepaper">
|
|
160
|
+
Read the whitepaper
|
|
161
|
+
</a>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div className="justify-self-center w-[180px]">
|
|
165
|
+
<MeyQR
|
|
166
|
+
url={qrUrl}
|
|
167
|
+
brand="cult"
|
|
168
|
+
fg="#ffffff"
|
|
169
|
+
accent="#FFE600"
|
|
170
|
+
bg={null}
|
|
171
|
+
plaque="none"
|
|
172
|
+
style="styled"
|
|
173
|
+
size={180}
|
|
174
|
+
label="QR code — MeyCult Unveil"
|
|
175
|
+
/>
|
|
176
|
+
<p className="mt-2 text-[10px] uppercase tracking-[0.25em] text-text-muted/60 text-center">
|
|
177
|
+
Pierce the veil
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function NotifyPanel() {
|
|
185
|
+
return (
|
|
186
|
+
<div className="landing-card p-8 max-w-lg mx-auto text-center">
|
|
187
|
+
<p className="text-accent font-bold text-lg mb-2">Be there at 20:00.</p>
|
|
188
|
+
<p className="text-sm text-text-muted mb-6">
|
|
189
|
+
One email when the veil lifts. No spam. No password.
|
|
190
|
+
</p>
|
|
191
|
+
<a
|
|
192
|
+
className="inline-block px-6 py-2.5 rounded-lg bg-accent text-bg font-bold text-sm uppercase tracking-wider"
|
|
193
|
+
href="/#crowdfunding"
|
|
194
|
+
>
|
|
195
|
+
Join the list
|
|
196
|
+
</a>
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|