mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-21 21:55:27 +00:00
feat: add search bar for /discovery and seed.sql for populating random data
This commit is contained in:
77
database/seed.sql
Normal file
77
database/seed.sql
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Optional: start clean (will also remove RSVPs via CASCADE)
|
||||||
|
TRUNCATE TABLE events CASCADE;
|
||||||
|
|
||||||
|
-- -----------------------------
|
||||||
|
-- Seed 100 events
|
||||||
|
-- -----------------------------
|
||||||
|
WITH params AS (
|
||||||
|
SELECT
|
||||||
|
ARRAY[
|
||||||
|
'Budapest', 'Berlin', 'Paris', 'Madrid', 'Rome', 'Vienna', 'Prague',
|
||||||
|
'Warsaw', 'Amsterdam', 'Lisbon', 'Copenhagen', 'Dublin', 'Athens',
|
||||||
|
'Zurich', 'Helsinki', 'Oslo', 'Stockholm', 'Brussels', 'Munich', 'Milan'
|
||||||
|
]::text[] AS cities,
|
||||||
|
ARRAY['Hall','Park','Rooftop','Auditorium','Conference Center','Café','Online']::text[] AS venues,
|
||||||
|
ARRAY['Tech Talk','Meetup','Workshop','Concert','Yoga','Brunch','Game Night','Hackathon','Book Club','Networking']::text[] AS themes
|
||||||
|
),
|
||||||
|
to_insert AS (
|
||||||
|
SELECT
|
||||||
|
-- 8-char ID (hex)
|
||||||
|
LEFT(ENCODE(gen_random_bytes(4), 'hex'), 8) AS id,
|
||||||
|
-- Make varied names by mixing a theme and city
|
||||||
|
(SELECT themes[(gs % array_length(themes,1)) + 1] FROM params) || ' @ ' ||
|
||||||
|
(SELECT cities[(gs % array_length(cities,1)) + 1] FROM params) AS name,
|
||||||
|
-- Spread dates across past and future (centered around today)
|
||||||
|
(CURRENT_DATE + (gs - 50))::date AS date,
|
||||||
|
-- Varied times: between 08:00 and 19:xx
|
||||||
|
make_time( (8 + (gs % 12))::int, (gs*7 % 60)::int, 0)::time AS time,
|
||||||
|
-- City + venue
|
||||||
|
(SELECT cities[(gs % array_length(cities,1)) + 1] FROM params) || ' ' ||
|
||||||
|
(SELECT venues[(gs % array_length(venues,1)) + 1] FROM params) AS location,
|
||||||
|
-- Alternate types
|
||||||
|
CASE WHEN gs % 2 = 0 THEN 'limited' ELSE 'unlimited' END AS type,
|
||||||
|
-- Only set attendee_limit for limited events
|
||||||
|
CASE WHEN gs % 2 = 0 THEN 10 + (gs % 40) ELSE NULL END AS attendee_limit,
|
||||||
|
-- Rotate through 20 user_ids
|
||||||
|
'user_' || ((gs % 20) + 1)::text AS user_id,
|
||||||
|
-- Mix public/private
|
||||||
|
CASE WHEN gs % 3 = 0 THEN 'private' ELSE 'public' END AS visibility
|
||||||
|
FROM generate_series(1, 100) AS gs
|
||||||
|
)
|
||||||
|
INSERT INTO events (id, name, date, time, location, type, attendee_limit, user_id, visibility, created_at, updated_at)
|
||||||
|
SELECT id, name, date, time, location, type, attendee_limit, user_id, visibility, NOW(), NOW()
|
||||||
|
FROM to_insert;
|
||||||
|
|
||||||
|
-- -----------------------------
|
||||||
|
-- Seed RSVPs
|
||||||
|
-- - For limited events: 0..attendee_limit attendees
|
||||||
|
-- - For unlimited events: 0..75 attendees
|
||||||
|
-- -----------------------------
|
||||||
|
WITH ev AS (
|
||||||
|
SELECT e.id, e.type, e.attendee_limit
|
||||||
|
FROM events e
|
||||||
|
),
|
||||||
|
counts AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
attendee_limit,
|
||||||
|
CASE
|
||||||
|
WHEN type = 'limited' THEN GREATEST(0, LEAST(attendee_limit, FLOOR(random() * (COALESCE(attendee_limit,0) + 1))::int))
|
||||||
|
ELSE FLOOR(random() * 76)::int
|
||||||
|
END AS rsvp_count
|
||||||
|
FROM ev
|
||||||
|
)
|
||||||
|
INSERT INTO rsvps (event_id, name, user_id, created_at, updated_at)
|
||||||
|
SELECT
|
||||||
|
c.id AS event_id,
|
||||||
|
'Attendee ' || c.id || '-' || g AS name,
|
||||||
|
-- distribute user_ids across 200 synthetic users, deterministically mixed per event
|
||||||
|
'user_' || ((ABS(HASHTEXT(c.id)) + g) % 200 + 1)::text AS user_id,
|
||||||
|
NOW(), NOW()
|
||||||
|
FROM counts c
|
||||||
|
JOIN LATERAL generate_series(1, c.rsvp_count) AS g ON TRUE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -6,10 +6,19 @@
|
|||||||
|
|
||||||
let publicEvents: Event[] = [];
|
let publicEvents: Event[] = [];
|
||||||
let error = '';
|
let error = '';
|
||||||
|
let searchQuery = '';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
// Use the server-side data
|
// Use the server-side data
|
||||||
$: publicEvents = data.events;
|
$: publicEvents = data.events;
|
||||||
|
|
||||||
|
// Filter events based on search query
|
||||||
|
$: filteredEvents =
|
||||||
|
searchQuery.trim() === ''
|
||||||
|
? publicEvents
|
||||||
|
: publicEvents.filter((event) =>
|
||||||
|
event.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -48,12 +57,54 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="mx-auto max-w-4xl">
|
<div class="mx-auto max-w-4xl">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-2xl font-bold text-slate-300">Public Events ({publicEvents.length})</h2>
|
<h2 class="text-2xl font-bold text-slate-300">Public Events ({filteredEvents.length})</h2>
|
||||||
<p class="text-slate-500">Discover events created by the community</p>
|
<p class="text-slate-500">Discover events created by the community</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="relative mx-auto max-w-md">
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
|
<svg
|
||||||
|
class="h-5 w-5 text-slate-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
placeholder="Search events by title..."
|
||||||
|
class="w-full rounded-lg border border-slate-600 bg-slate-800 px-4 py-3 pl-10 text-white placeholder-slate-400 focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none"
|
||||||
|
/>
|
||||||
|
{#if searchQuery}
|
||||||
|
<button
|
||||||
|
on:click={() => (searchQuery = '')}
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center pr-3 text-slate-400 hover:text-slate-300"
|
||||||
|
>
|
||||||
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{#each publicEvents as event, i (i)}
|
{#each filteredEvents as event, i (i)}
|
||||||
<div class="rounded-sm border border-slate-200 p-6 shadow-sm">
|
<div class="rounded-sm border border-slate-200 p-6 shadow-sm">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="mb-2 text-xl font-bold text-slate-300">{event.name}</h3>
|
<h3 class="mb-2 text-xl font-bold text-slate-300">{event.name}</h3>
|
||||||
@@ -110,6 +161,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if searchQuery && filteredEvents.length === 0}
|
||||||
|
<div class="mt-8 text-center">
|
||||||
|
<div class="mb-4 text-4xl">🔍</div>
|
||||||
|
<h3 class="mb-2 text-xl font-bold text-slate-300">No events found</h3>
|
||||||
|
<p class="text-slate-500">Try adjusting your search terms or browse all events</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user