Skip to content

Commit 49a3e00

Browse files
Details filter & RED → GREEN READINESS CHAIN
1 parent e696961 commit 49a3e00

3 files changed

Lines changed: 651 additions & 19 deletions

File tree

src/components/Details.svelte

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
4+
// schema: one entry from formSchema.ts, or null/undefined if this
5+
// model has no schema yet (see the 'vehicle_ride_sharing' gap note
6+
// there) — Details renders a small fallback instead of guessing.
7+
export let schema = null;
8+
9+
// values: the field state, lifted and owned by the parent (HexMenu),
10+
// same pattern as Anypay's `selected` — Details never holds its own
11+
// copy, it only dispatches what changed and lets the parent decide
12+
// what to do with it.
13+
export let values = {};
14+
15+
const dispatch = createEventDispatcher();
16+
17+
const MODE_OPTIONS = [
18+
{ id: 'in_person', label: 'In-Person' },
19+
{ id: 'online', label: 'Online' },
20+
{ id: 'both', label: 'Both' },
21+
];
22+
23+
function set(key, value) {
24+
dispatch('update', { key, value });
25+
}
26+
27+
function pickCategory(id) {
28+
if (schema.category.multi) {
29+
const cur = values.categoryIds || [];
30+
const next = cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id];
31+
set('categoryIds', next);
32+
} else {
33+
set('categoryId', id);
34+
}
35+
}
36+
37+
function isCategorySelected(id) {
38+
return schema.category.multi
39+
? (values.categoryIds || []).includes(id)
40+
: values.categoryId === id;
41+
}
42+
43+
function onBackdrop(e) {
44+
if (e.target === e.currentTarget) dispatch('close');
45+
}
46+
</script>
47+
48+
<div class="backdrop" on:click={onBackdrop}>
49+
<div class="modal" role="dialog" aria-modal="true" aria-label="Details">
50+
<button class="close" on:click={() => dispatch('close')} aria-label="Close">✕</button>
51+
52+
{#if !schema}
53+
<h2>DETAILS</h2>
54+
<p class="hint">No fields defined for this model yet.</p>
55+
{:else}
56+
<h2>DETAILS</h2>
57+
<p class="hint">Fill in what applies, then close with ✕.</p>
58+
59+
{#if schema.modeSelector}
60+
<div class="mode-row">
61+
{#each MODE_OPTIONS as m}
62+
<button
63+
class="mode-opt"
64+
class:selected={(values.mode || 'in_person') === m.id}
65+
on:click={() => set('mode', m.id)}
66+
>{m.label}</button>
67+
{/each}
68+
</div>
69+
{/if}
70+
71+
<label class="field-label" for="details-title">{schema.titleLabel} *</label>
72+
<input
73+
id="details-title"
74+
class="text-input"
75+
placeholder={schema.titlePlaceholder}
76+
value={values.title || ''}
77+
on:input={(e) => set('title', e.currentTarget.value)}
78+
/>
79+
80+
<span class="field-label">Category *</span>
81+
<div class="options">
82+
{#each schema.category.options as opt}
83+
<button
84+
class="option"
85+
class:selected={isCategorySelected(opt.id)}
86+
on:click={() => pickCategory(opt.id)}
87+
>
88+
<span class="opt-name">{opt.name}</span>
89+
{#if opt.description}<span class="opt-desc">{opt.description}</span>{/if}
90+
</button>
91+
{/each}
92+
</div>
93+
94+
{#if schema.date}
95+
<label class="field-label" for="details-date">
96+
{schema.date.label || 'Date & Time'}{schema.date.required ? ' *' : ''}
97+
</label>
98+
<input
99+
id="details-date"
100+
class="text-input"
101+
type="datetime-local"
102+
value={values.date || ''}
103+
on:input={(e) => set('date', e.currentTarget.value)}
104+
/>
105+
{#if !schema.date.required}
106+
<p class="subtle">Optional — leave empty for recurring or open-ended.</p>
107+
{/if}
108+
{/if}
109+
110+
<label class="field-label" for="details-description">Description *</label>
111+
<textarea
112+
id="details-description"
113+
class="textarea"
114+
placeholder={schema.descriptionPlaceholder}
115+
value={values.description || ''}
116+
on:input={(e) => set('description', e.currentTarget.value)}
117+
></textarea>
118+
119+
<label class="field-label" for="details-contact">Contact Link *</label>
120+
<input
121+
id="details-contact"
122+
class="text-input"
123+
placeholder="https://t.me/you or https://wa.me/123..."
124+
value={values.contact || ''}
125+
on:input={(e) => set('contact', e.currentTarget.value)}
126+
/>
127+
<p class="subtle">{schema.contactHint}</p>
128+
{/if}
129+
</div>
130+
</div>
131+
132+
<style>
133+
.backdrop {
134+
position: fixed;
135+
inset: 0;
136+
background: rgba(0, 0, 0, 0.6);
137+
display: flex;
138+
align-items: center;
139+
justify-content: center;
140+
z-index: 50;
141+
padding: 24px 0;
142+
}
143+
144+
.modal {
145+
position: relative;
146+
background: #161616;
147+
border: 1px solid #333;
148+
border-radius: 14px;
149+
padding: 28px 24px 24px;
150+
width: min(360px, calc(100vw - 40px));
151+
max-height: calc(100vh - 48px);
152+
overflow-y: auto;
153+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
154+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
155+
}
156+
157+
h2 {
158+
margin: 0 0 6px;
159+
color: #fff;
160+
font-size: 1em;
161+
letter-spacing: 1.5px;
162+
text-align: center;
163+
}
164+
165+
.hint {
166+
margin: 0 0 18px;
167+
color: #888;
168+
font-size: 0.78em;
169+
text-align: center;
170+
}
171+
172+
.close {
173+
position: absolute;
174+
top: 10px;
175+
right: 10px;
176+
background: none;
177+
border: none;
178+
color: #888;
179+
font-size: 1em;
180+
cursor: pointer;
181+
line-height: 1;
182+
padding: 6px;
183+
}
184+
.close:hover { color: #fff; }
185+
186+
.mode-row {
187+
display: flex;
188+
gap: 6px;
189+
margin-bottom: 18px;
190+
}
191+
.mode-opt {
192+
flex: 1;
193+
border: 1.5px solid #333;
194+
background: #111;
195+
color: #aaa;
196+
border-radius: 8px;
197+
padding: 8px 4px;
198+
font-size: 0.78em;
199+
font-weight: 600;
200+
cursor: pointer;
201+
}
202+
.mode-opt.selected {
203+
background: rgba(51, 91, 244, 0.38);
204+
border-color: #8fb0ff;
205+
color: #fff;
206+
}
207+
208+
.field-label {
209+
display: block;
210+
margin: 14px 0 6px;
211+
color: #ccc;
212+
font-size: 0.8em;
213+
font-weight: 600;
214+
}
215+
216+
.text-input, .textarea {
217+
width: 100%;
218+
box-sizing: border-box;
219+
background: #0d0d0d;
220+
border: 1px solid #333;
221+
border-radius: 8px;
222+
padding: 10px 12px;
223+
color: #fff;
224+
font-size: 0.9em;
225+
font-family: inherit;
226+
}
227+
.text-input::placeholder, .textarea::placeholder { color: #666; }
228+
.textarea { min-height: 80px; resize: vertical; }
229+
230+
.subtle {
231+
margin: 6px 0 0;
232+
color: #777;
233+
font-size: 0.72em;
234+
}
235+
236+
.options {
237+
display: flex;
238+
flex-direction: column;
239+
gap: 8px;
240+
}
241+
242+
.option {
243+
display: flex;
244+
flex-direction: column;
245+
align-items: flex-start;
246+
gap: 2px;
247+
border: 1.5px solid;
248+
border-image: linear-gradient(90deg, #335bf4, #2ae9c9) 1;
249+
background: #111;
250+
color: #fff;
251+
border-radius: 10px;
252+
padding: 10px 12px;
253+
text-align: left;
254+
cursor: pointer;
255+
transition: background 0.2s;
256+
}
257+
.option.selected {
258+
background: rgba(51, 91, 244, 0.38);
259+
border-image: none;
260+
border-color: #8fb0ff;
261+
}
262+
.opt-name {
263+
font-size: 0.9em;
264+
font-weight: 600;
265+
letter-spacing: 0.3px;
266+
}
267+
.opt-desc {
268+
font-size: 0.74em;
269+
color: #9aa4b2;
270+
}
271+
</style>

0 commit comments

Comments
 (0)