-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSnfSitesPage.jsx
More file actions
180 lines (170 loc) · 6.67 KB
/
Copy pathSnfSitesPage.jsx
File metadata and controls
180 lines (170 loc) · 6.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import React, { useEffect, useMemo, useState } from 'react';
import { MapTrifoldIcon, MapPinIcon, CrosshairIcon } from '@phosphor-icons/react';
import SnfLayout from '../../components/snf/SnfLayout';
import {
SnfPageHeader,
SnfSearchInput,
SnfReveal,
SnfEmpty,
SnfPill,
SnfModal,
} from '../../components/snf/SnfPrimitives';
import Seo from '../../components/Seo';
import { useSnf } from '../../context/SnfContext';
import { usePlaces } from '../../hooks/useStoriesData';
const SnfSitesPage = () => {
const { language, setBreadcrumbs } = useSnf();
const { data: places, loading } = usePlaces();
const [query, setQuery] = useState('');
const [active, setActive] = useState(null);
useEffect(() => {
setBreadcrumbs([{ label: 'SITES' }]);
}, [setBreadcrumbs]);
const grouped = useMemo(() => {
const term = query.toLowerCase();
const filtered = (places || []).filter(
(p) =>
p.name?.toLowerCase().includes(term) ||
p.type?.toLowerCase().includes(term) ||
p.description?.toLowerCase().includes(term),
);
const map = filtered.reduce((acc, p) => {
const cat = p.category || 'UNCHARTED';
(acc[cat] = acc[cat] || []).push(p);
return acc;
}, {});
return Object.keys(map)
.sort()
.map((cat) => ({ cat, items: map[cat] }));
}, [places, query]);
const total = grouped.reduce((n, g) => n + g.items.length, 0);
return (
<SnfLayout>
<Seo
title="Sites | Serfs and Frauds Terminal"
description="Mapped locations from the From Serfs and Frauds anthology."
keywords={['serfs and frauds', 'places', 'sites', 'locations']}
/>
<SnfPageHeader
fileNo="003"
label="MAPPED LOCATIONS"
title={language === 'tr' ? 'BÖLGELER' : 'SITES'}
subtitle={
language === 'tr'
? 'Bölge genelinde işaretli konumlar.'
: 'Pinned locations across the territory.'
}
icon={<MapTrifoldIcon size={40} weight="duotone" />}
/>
<div className="max-w-xl mb-10">
<SnfSearchInput
value={query}
onChange={setQuery}
placeholder={
language === 'tr' ? 'konum ara…' : 'query by name / type / note…'
}
/>
</div>
{loading && (
<div className="snf-mono text-sm snf-dim animate-pulse">
triangulating sites…
</div>
)}
<div className="space-y-12">
{grouped.map((group) => (
<section key={group.cat}>
<div className="flex items-center gap-3 mb-5">
<CrosshairIcon
size={18}
weight="bold"
className="snf-phos-text flex-none"
/>
<h2 className="snf-display text-lg md:text-xl font-bold uppercase tracking-wide text-[var(--snf-ink)]">
{group.cat}
</h2>
<span className="snf-mono text-[10px] snf-dim tabular-nums">
[{String(group.items.length).padStart(2, '0')}]
</span>
<span className="snf-divider flex-grow" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{group.items.map((p, i) => (
<SnfReveal key={`${p.name}-${i}`} delay={Math.min(i * 0.04, 0.25)}>
<button
type="button"
onClick={() => setActive(p)}
className="snf-panel snf-panel-bracket text-left w-full h-full p-5 flex flex-col hover:border-[rgba(var(--snf-phos-rgb),0.5)] transition-colors group"
>
<div className="flex items-center justify-between mb-3">
<span className="snf-mono text-[10px] tracking-[0.16em] snf-phos-text flex items-center gap-1.5">
<MapPinIcon size={12} weight="fill" /> {p.type}
</span>
<SnfPill status={p.status} />
</div>
<div className="snf-display text-lg font-bold uppercase tracking-wide text-[var(--snf-ink)] group-hover:text-[var(--snf-phos)] transition-colors leading-tight mb-3">
{p.name}
</div>
<p className="snf-mono text-[11px] snf-dim leading-relaxed line-clamp-3 flex-grow">
{p.description}
</p>
<span className="snf-mono text-[9px] uppercase tracking-[0.3em] snf-dim group-hover:text-[var(--snf-phos)] transition-colors mt-4">
{language === 'tr' ? 'KONUMU AÇ ▸' : 'OPEN SITE ▸'}
</span>
</button>
</SnfReveal>
))}
</div>
</section>
))}
{!loading && total === 0 && (
<SnfEmpty>
{language === 'tr' ? 'Konum bulunamadı.' : 'No sites located.'}
</SnfEmpty>
)}
</div>
<SnfModal
open={!!active}
onClose={() => setActive(null)}
kicker={active ? `SITE :: ${active.name}` : ''}
>
{active && (
<div>
<div className="flex items-start justify-between gap-4 mb-2">
<h2 className="snf-display snf-glow text-3xl md:text-4xl font-bold uppercase leading-none text-[var(--snf-phos)]">
{active.name}
</h2>
<SnfPill status={active.status} />
</div>
<div className="snf-mono text-[11px] tracking-[0.16em] snf-phos-text mb-5 flex items-center gap-1.5">
<MapPinIcon size={12} weight="fill" /> {active.type} ·{' '}
{active.category}
</div>
<div className="snf-divider mb-5" />
<p className="snf-serif text-base md:text-lg text-[var(--snf-ink)] leading-relaxed mb-6">
{active.description}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="snf-panel p-3">
<div className="snf-mono text-[9px] tracking-[0.2em] snf-dim mb-1">
STATUS
</div>
<div className="snf-display text-[var(--snf-phos)] font-bold">
{active.status}
</div>
</div>
<div className="snf-panel p-3">
<div className="snf-mono text-[9px] tracking-[0.2em] snf-dim mb-1">
FIRST LOGGED
</div>
<div className="snf-mono text-sm text-[var(--snf-ink)]">
{active.book}
</div>
</div>
</div>
</div>
)}
</SnfModal>
</SnfLayout>
);
};
export default SnfSitesPage;