feat: add map + under construction pages (#11)

* feat: add map

* feat: move map to top

* feat: move map + 404

* feat: add record id to table

* chore: prisma generated files

* feat: record record ids

* feat: fillout events param
This commit is contained in:
Manitej Boorgu 2026-01-20 22:26:42 -05:00 committed by GitHub
parent f4d6684426
commit ae4b1e8519
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 626 additions and 39 deletions

View file

@ -15,13 +15,16 @@
"@prisma/client": "^7.1.0", "@prisma/client": "^7.1.0",
"@prisma/config": "^7.1.0", "@prisma/config": "^7.1.0",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/leaflet": "^1.9.21",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"astro": "^5.16.5", "astro": "^5.16.5",
"better-auth": "^1.4.6", "better-auth": "^1.4.6",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"leaflet": "^1.9.4",
"react": "^19.2.1", "react": "^19.2.1",
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"react-leaflet": "^5.0.0",
"svelte": "^5.45.10", "svelte": "^5.45.10",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3" "typescript": "^5.9.3"
@ -1415,6 +1418,17 @@
"@prisma/debug": "7.1.0" "@prisma/debug": "7.1.0"
} }
}, },
"node_modules/@react-leaflet/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27", "version": "1.0.0-beta.27",
"license": "MIT" "license": "MIT"
@ -2148,6 +2162,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/hast": { "node_modules/@types/hast": {
"version": "3.0.4", "version": "3.0.4",
"license": "MIT", "license": "MIT",
@ -2155,6 +2175,15 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"node_modules/@types/leaflet": {
"version": "1.9.21",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/mdast": { "node_modules/@types/mdast": {
"version": "4.0.4", "version": "4.0.4",
"license": "MIT", "license": "MIT",
@ -3879,6 +3908,13 @@
"node": ">=20.0.0" "node": ">=20.0.0"
} }
}, },
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.2", "version": "1.30.2",
"license": "MPL-2.0", "license": "MPL-2.0",
@ -5381,6 +5417,20 @@
"react": "^19.2.1" "react": "^19.2.1"
} }
}, },
"node_modules/react-leaflet": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^3.0.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"license": "MIT", "license": "MIT",

View file

@ -17,13 +17,16 @@
"@prisma/client": "^7.1.0", "@prisma/client": "^7.1.0",
"@prisma/config": "^7.1.0", "@prisma/config": "^7.1.0",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/leaflet": "^1.9.21",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"astro": "^5.16.5", "astro": "^5.16.5",
"better-auth": "^1.4.6", "better-auth": "^1.4.6",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"leaflet": "^1.9.4",
"react": "^19.2.1", "react": "^19.2.1",
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"react-leaflet": "^5.0.0",
"svelte": "^5.45.10", "svelte": "^5.45.10",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3" "typescript": "^5.9.3"

View file

@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[recordId]` on the table `Satellite` will be added. If there are existing duplicate values, this will fail.
- Added the required column `recordId` to the `Satellite` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Satellite" ADD COLUMN "recordId" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Satellite_recordId_key" ON "Satellite"("recordId");

View file

@ -15,6 +15,7 @@ datasource db {
model Satellite { model Satellite {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
recordId String @unique
slug String @unique slug String @unique
data Json data Json
active Boolean @default(true) active Boolean @default(true)

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View file

@ -5,9 +5,11 @@ import Step from '../primitives/Step.js';
import StoryCard from '../primitives/StoryCard.js'; import StoryCard from '../primitives/StoryCard.js';
import GameCard from '../primitives/GameCard.js'; import GameCard from '../primitives/GameCard.js';
import NavbarLink from '../primitives/NavbarLink.tsx'; import NavbarLink from '../primitives/NavbarLink.tsx';
import VideoEmbed from '../primitives/VideoEmbed.tsx'; import MapEmbed from '../primitives/MapEmbed.tsx';
import { Map } from '../primitives/Map.tsx';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import type { EventLocation } from '../../lib/events.ts';
const FORM_URL_ORGANIZER_APPLICATION = "https://forms.hackclub.com/t/8L51MzWyrHus"; const FORM_URL_ORGANIZER_APPLICATION = "https://forms.hackclub.com/t/8L51MzWyrHus";
const FORM_URL_RSVP = "https://forms.hackclub.com/t/a3QSt8MuvHus"; const FORM_URL_RSVP = "https://forms.hackclub.com/t/a3QSt8MuvHus";
@ -49,12 +51,13 @@ function FlagshipCTA({ className, compact, maxWidth }: { className?: string; com
); );
} }
function App() { function App({ events }: { events: EventLocation[] }) {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [scrollY, setScrollY] = useState(document.body.scrollTop); const [scrollY, setScrollY] = useState(document.body.scrollTop);
const [isLargeScreen, setIsLargeScreen] = useState(window.innerWidth >= 1280); const [isLargeScreen, setIsLargeScreen] = useState(window.innerWidth >= 1280);
const [windowHeight, setWindowHeight] = useState(window.innerHeight); const [windowHeight, setWindowHeight] = useState(window.innerHeight);
const [windowWidth, setWindowWidth] = useState(window.innerWidth); const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [isMapOpen, setIsMapOpen] = useState(false);
const emailRef = useRef<HTMLInputElement>(null); const emailRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@ -111,7 +114,7 @@ function App() {
/> />
</div> </div>
<div className="w-full h-screen"> <div className={clsx("w-full relative", windowHeight > windowWidth && windowWidth < 860 ? "h-full" : "h-screen")}>
<header className="relative h-[60px] md:h-[115px] bg-[#45b4f5] justify-end items-center content-center md:pr-16 hidden sm:flex"> <header className="relative h-[60px] md:h-[115px] bg-[#45b4f5] justify-end items-center content-center md:pr-16 hidden sm:flex">
<nav className="flex gap-4 w-full justify-between px-8 md:px-0 text-2xl md:gap-12 items-center md:justify-end text-white md:text-3xl xl:text-5xl font-bold font-ember-and-fire"> <nav className="flex gap-4 w-full justify-between px-8 md:px-0 text-2xl md:gap-12 items-center md:justify-end text-white md:text-3xl xl:text-5xl font-bold font-ember-and-fire">
<NavbarLink onClick={() => scrollToSection('steps')}>How to organize</NavbarLink> <NavbarLink onClick={() => scrollToSection('steps')}>How to organize</NavbarLink>
@ -124,8 +127,8 @@ function App() {
</header> </header>
<section className={clsx( <section className={clsx(
"relative h-full px-6 md:px-16 md:px-24 2xl:px-32 bg-[url(/backgrounds/blue-sky.webp)] bg-center bg-cover", "relative px-6 md:px-16 md:px-24 2xl:px-32 bg-[url(/backgrounds/blue-sky.webp)] bg-center bg-cover",
windowHeight > windowWidth && windowWidth < 860 ? "flex items-stretch pb-16" : "flex items-end pb-32 2xl:pb-48" windowHeight > windowWidth && windowWidth < 860 ? "flex items-stretch pb-0" : "h-full flex items-end pb-32 2xl:pb-48"
)}> )}>
<div className="absolute top-0 left-0 w-full h-full pointer-events-none"> <div className="absolute top-0 left-0 w-full h-full pointer-events-none">
<img src="/backgrounds/sky-shine.webp" alt="" className="w-full h-full object-cover select-none" /> <img src="/backgrounds/sky-shine.webp" alt="" className="w-full h-full object-cover select-none" />
@ -159,21 +162,24 @@ function App() {
/> />
</div> </div>
<div className="absolute -bottom-[50px] left-0 w-full h-[120px] pointer-events-none"> {/* <div className="absolute -bottom-[50px] left-0 w-full h-[120px] pointer-events-none">
<img <img
src="/decorative/vines.webp" src="/decorative/vines.webp"
alt="" alt=""
className="w-full h-full object-cover object-top select-none" className="w-full h-full object-cover object-top select-none"
/> />
</div> </div> */}
<div className="flex flex-col md:flex-row justify-between items-center md:items-start xl:items-center w-full gap-8 pb-16 z-30 h-full pt-16 md:pt-0 md:h-auto"> <div className={clsx(
"flex flex-col md:flex-row justify-between items-center md:items-start xl:items-center w-full gap-8 z-30 pt-16 md:pt-0",
windowHeight > windowWidth && windowWidth < 860 ? "pb-32" : "pb-16 h-full md:h-auto"
)}>
<div className={clsx( <div className={clsx(
"flex flex-col gap-4 w-full md:w-[648px]", "flex flex-col gap-4 w-full md:w-[648px]",
windowHeight > windowWidth && windowWidth < 860 && "justify-between h-full" windowHeight > windowWidth && windowWidth < 860 && "justify-between h-full"
)}> )}>
<div className="mb-6"> <div className="mb-6">
{windowWidth >= 400 && <FlagshipCTA className="min-[860px]:hidden -mt-12 mb-8" compact={windowWidth < 500} />} {/* {windowWidth >= 400 && <FlagshipCTA className="min-[860px]:hidden -mt-12 mb-8" compact={windowWidth < 500} />} */}
<div className="flex items-center gap-3 mb-4 relative z-30"> <div className="flex items-center gap-3 mb-4 relative z-30">
<a href='https://hackclub.com' className='transition-transform hover:scale-105 active:scale-95'> <a href='https://hackclub.com' className='transition-transform hover:scale-105 active:scale-95'>
@ -280,7 +286,7 @@ function App() {
</div> </div>
</div> </div>
<VideoEmbed className="hidden xl:block self-end mb-8" /> <MapEmbed className="hidden xl:block self-end mb-8 relative z-50" onOpenMap={() => setIsMapOpen(true)} />
</div> </div>
</section> </section>
@ -297,8 +303,8 @@ function App() {
</div> </div>
<section className="relative pb-96 bg-[url(/backgrounds/underwater-gradient.webp)] bg-cover bg-top"> <section className="relative pb-96 bg-[url(/backgrounds/underwater-gradient.webp)] bg-cover bg-top">
<div className="xl:hidden pt-16 pb-8 relative z-10"> <div className="xl:hidden pt-16 pb-8 relative z-50">
<VideoEmbed className="px-6" /> <MapEmbed className="px-6 relative z-50 max-w-sm mx-auto" onOpenMap={() => setIsMapOpen(true)} />
</div> </div>
<div className="pt-[8vw] xl:pt-[13vw]"></div> <div className="pt-[8vw] xl:pt-[13vw]"></div>
<div className="absolute top-0 left-0 w-screen h-[200px] bg-gradient-to-b from-[#004b2a] to-transparent pointer-events-none"></div> <div className="absolute top-0 left-0 w-screen h-[200px] bg-gradient-to-b from-[#004b2a] to-transparent pointer-events-none"></div>
@ -709,6 +715,29 @@ function App() {
</div> </div>
</div> </div>
</footer> </footer>
{isMapOpen && (
<div
className="fixed inset-0 bg-black/80 z-[9999] flex items-center justify-center p-4"
onClick={() => setIsMapOpen(false)}
>
<div
className="relative w-[90vw] h-[80vh]"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => setIsMapOpen(false)}
className="absolute -top-10 right-0 text-white text-3xl font-bold hover:opacity-70 cursor-pointer"
>
</button>
<Map
events={events}
className="w-full h-full rounded-2xl"
/>
</div>
</div>
)}
</div> </div>
); );
} }

View file

@ -3,7 +3,7 @@ import FaqButton from '../primitives/FaqButton.js';
import Step from '../primitives/Step.js'; import Step from '../primitives/Step.js';
import GameCard from '../primitives/GameCard.js'; import GameCard from '../primitives/GameCard.js';
import NavbarLink from '../primitives/NavbarLink.tsx'; import NavbarLink from '../primitives/NavbarLink.tsx';
import VideoEmbed from '../primitives/VideoEmbed.tsx'; import VideoEmbed from '../primitives/MapEmbed.tsx';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import type { SatelliteContent } from '../../lib/satellite.ts'; import type { SatelliteContent } from '../../lib/satellite.ts';

View file

@ -0,0 +1,153 @@
import '../../styles/global.css';
import { useState, useRef } from 'react';
import clsx from 'clsx';
const FORM_URL_SIGN_UP = "https://forms.hackclub.com/campfire-signup";
interface UnderConstructionProps {
event_name: string;
record_id?: string;
}
function UnderConstruction({ event_name, record_id }: UnderConstructionProps) {
const [email, setEmail] = useState("");
const emailRef = useRef<HTMLInputElement>(null);
function openWithEmail(url: string) {
if (!emailRef?.current?.reportValidity() || !email)
return;
window.open(`${url}?email=${encodeURIComponent(email)}&event=${encodeURIComponent(record_id || "")}`, "_blank");
}
return (
<div className="w-full min-h-screen flex flex-col overflow-hidden relative bg-[#fca84a]">
{/* Background layers */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<img
src="/backgrounds/blue-sky.webp"
alt=""
className="absolute inset-0 w-full h-full object-cover select-none"
/>
<img
src="/backgrounds/sky-shine.webp"
alt=""
className="absolute inset-0 w-full h-full object-cover select-none"
/>
<img
src="/backgrounds/landing-grass.png"
alt=""
className="absolute bottom-0 left-0 w-full h-auto object-cover select-none"
/>
</div>
{/* Content */}
<div className="relative z-10 flex flex-col items-center justify-center flex-1 px-6 py-16">
<div className="flex flex-col gap-8 items-center max-w-[648px] w-full">
{/* Logo section */}
<div className="flex md:block flex-col items-center justify-center">
<div className="flex items-center gap-3 mb-4 relative z-30">
<a href='https://hackclub.com' className='transition-transform hover:scale-105 active:scale-95'>
<img
src="/decorative/flag-standalone-wtransparent.png"
alt="Hack Club"
className="w-[151px] h-[53px] object-cover transform rotate-[-4.8deg] select-none"
style={{
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
}}
/>
</a>
<div className="w-[2px] h-8 bg-white opacity-60"></div>
<a href='https://opensauce.com' className='transition-transform scale-125 hover:scale-130 active:scale-130'>
<img
src="/branding/logo-opensauce.webp"
alt="Open Sauce"
className="h-[70px] object-contain select-none pl-4"
style={{
filter: "drop-shadow(3px 3px 0px rgba(0,0,0,0.25))"
}}
/>
</a>
</div>
<div className="transform md:rotate-[-2.97deg] w-min">
<h1
className="text-[#fcf5ed] text-[80px] md:text-[100px] xl:text-[150px] font-normal leading-none -mb-4 font-dream-planner"
style={{
textShadow: "5px 8px 0px rgba(0,0,0,0.25)"
}}
>
CAMPFIRE
</h1>
<h3
className="text-[#fcf5ed] text-[40px] md:text-[50px] xl:text-[60px] font-normal leading-none mb-4 font-dream-planner text-right"
style={{
textShadow: "5px 8px 0px rgba(0,0,0,0.25)"
}}
>
{event_name.toUpperCase()}
</h3>
</div>
<div className="pl-2 md:pl-4">
<p
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold mb-2 font-ember-and-fire text-center"
style={{
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
}}
>
This site is still under construction!
</p>
<p
className="text-white text-4xl md:text-3xl xl:text-4xl font-bold font-ember-and-fire text-center"
style={{
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
}}
>
You can still sign up below:
</p>
</div>
</div>
{/* RSVP form */}
<div className='flex flex-col gap-4'>
<div className="flex flex-col md:flex-row items-center gap-2">
<div
className={clsx(
"bg-[#f9e2ca] border-4 border-[#d5a16c] rounded-[20px] px-4 md:px-8 py-4 flex items-center gap-3 md:gap-6 w-full transform md:rotate-[-1.2deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)]",
"transition-transform hover:scale-105"
)}
>
<img src="/icons/email.svg" alt="" className="w-6 h-5 flex-shrink-0 select-none" />
<input
required
ref={emailRef}
value={email}
onChange={e => setEmail(e.target.value)}
type="email"
className="text-[#854d16] text-2xl md:text-4xl font-bold truncate bg-transparent border-none outline-none flex-1 cursor-text font-ember-and-fire"
placeholder="you@hackclub.com"
/>
</div>
<button
className="bg-[#fca147] border-[5px] border-[rgba(0,0,0,0.2)] rounded-[20px] px-8 md:px-14 py-4 hover:scale-105 transition-transform w-full md:w-auto transform md:rotate-[1.5deg] shadow-[0_8px_20px_rgba(0,0,0,0.25)] cursor-pointer active:scale-95"
type="button"
onClick={() => openWithEmail(FORM_URL_SIGN_UP)}
>
<p
className="text-[#8d3f34] text-3xl md:text-5xl font-normal font-dream-planner whitespace-nowrap"
>
SIGN UP!
</p>
</button>
</div>
</div>
</div>
</div>
</div>
)
}
export default UnderConstruction;

View file

@ -0,0 +1,89 @@
import { useEffect, useState } from 'react';
import type { EventLocation } from '../../lib/events';
import '../../styles/map.css';
interface MapProps {
events: EventLocation[];
center?: [number, number];
zoom?: number;
className?: string;
}
export function Map({ events, center = [20, 0], zoom = 2, className }: MapProps) {
const [MapComponents, setMapComponents] = useState<{
MapContainer: any;
TileLayer: any;
Marker: any;
Popup: any;
useMap: any;
flagIcon: any;
} | null>(null);
useEffect(() => {
import('leaflet/dist/leaflet.css');
Promise.all([import('leaflet'), import('react-leaflet')]).then(([L, module]) => {
const flagIcon = L.icon({
iconUrl: '/map/map-flag.png',
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32]
});
setMapComponents({
MapContainer: module.MapContainer,
TileLayer: module.TileLayer,
Marker: module.Marker,
Popup: module.Popup,
useMap: module.useMap,
flagIcon,
});
});
}, []);
const validEvents = events.filter(
(event) => typeof event.lat === 'number' && typeof event.long === 'number'
);
if (!MapComponents) {
return <div style={{ minHeight: '400px', height: '100%', width: '100%', background: 'linear-gradient(135deg, #CCF4FD 0%, #B8D9F8 100%)', borderRadius: '16px' }} className={className} />;
}
const { MapContainer, TileLayer, Marker, Popup, useMap, flagIcon } = MapComponents;
function InvalidateSize() {
const map = useMap();
useEffect(() => {
const timer = setTimeout(() => map.invalidateSize(), 0);
const resizeTimer = setTimeout(() => map.invalidateSize(), 300);
return () => {
clearTimeout(timer);
clearTimeout(resizeTimer);
};
}, [map]);
return null;
}
return (
<MapContainer
center={center}
zoom={zoom}
className={className}
style={{ minHeight: '400px', height: '100%', width: '100%' }}
>
<InvalidateSize />
<TileLayer
attribution='© OpenStreetMap contributors © CARTO'
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
detectRetina={true}
/>
{validEvents.map((event) => (
<Marker key={event.slug} position={[event.lat, event.long]} icon={flagIcon}>
<Popup className="custom-popup">
<div>
<a href={`https://campfire.hackclub.com/${event.slug}`} className="text-black text-lg font-bold font-ember-and-fire">Campfire {event.event_name}</a>
</div>
</Popup>
</Marker>
))}
</MapContainer>
);
}

View file

@ -0,0 +1,40 @@
interface MapEmbedProps {
className?: string;
onOpenMap: () => void;
}
function MapEmbed({ className, onOpenMap }: MapEmbedProps) {
return (
<div className={className}>
<div className="flex items-center justify-center gap-3 mb-4 2xl:mb-8">
<p
className="text-white text-3xl 2xl:text-5xl font-bold font-ember-and-fire"
style={{
textShadow: "0px 4px 4px rgba(0,0,0,0.25)"
}}
>
find an event near you
</p>
<img
src="/compressed/ui/arrow.webp"
alt=""
className="w-[45px] md:w-[55px] h-[33px] md:h-[41px] translate-y-6 rotate-[6.2deg] z-50 select-none"
/>
</div>
<button
onClick={onOpenMap}
className="relative transform rotate-[1.7deg] transition-transform hover:scale-105 w-[70vw] md:w-[50vw] xl:w-[442px] mx-auto cursor-pointer"
>
<img
src="/map/map-screenshot.png"
alt="Map screenshot"
className="w-full rounded-2xl shadow-[12px_12px_0px_0px_rgba(0,0,0,0.25)]"
/>
</button>
</div>
);
}
export default MapEmbed;

80
astro/src/lib/events.ts Normal file
View file

@ -0,0 +1,80 @@
import 'dotenv/config';
const { AIRTABLE_API_KEY, AIRTABLE_BASE_ID } = process.env;
import Airtable from 'airtable';
let airtableBase: any = null;
if (AIRTABLE_API_KEY && AIRTABLE_BASE_ID) {
airtableBase = new Airtable({ apiKey: AIRTABLE_API_KEY }).base(AIRTABLE_BASE_ID);
}
export interface EventLocation {
slug: string;
lat: number;
long: number;
event_name: string;
}
interface AirtableRecord {
id: string;
fields: EventLocation;
}
export interface EventLocationWithDistance extends EventLocation {
distance: number;
}
export async function fetchEventsLoc() {
return (await airtableBase('events').select({
view: 'Everything',
// filterByFormula: '{website_active} = 1',
fields: [
'slug', //string
'event_name', //string
'lat', //number
'long', //number
'website_active' //boolean
]
}).all()) as AirtableRecord[]
}
// Cache to avoid multiple geocoding during build
let cachedEvents: EventLocation[] | null = null;
export async function loadEventsLoc(): Promise<EventLocation[]> {
// do nothing if the API keys aren't set
if (!AIRTABLE_API_KEY || !AIRTABLE_BASE_ID) {
return [];
}
if (cachedEvents) {
return cachedEvents;
}
console.log("Geocoding all events")
try {
// Fetch all approved events from Airtable with pagination
const locations = await fetchEventsLoc();
return locations.map(location => location.fields);
} catch (error) {
console.error('Failed to fetch event data:', error);
cachedEvents = [];
return [];
}
}
// Haversine formula to calculate distance between two points in miles
export function calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 3959; // Earth's radius in miles
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in miles
}

View file

@ -18,21 +18,6 @@ export async function getSatelliteData(slug: string) {
}); });
} }
export async function setSatelliteData(slug: string, data: any) {
return prisma.satellite.upsert({
where: {
slug,
},
create: {
slug,
data,
},
update: {
data,
},
});
}
export type SatelliteContent = { export type SatelliteContent = {
hero: { hero: {
title: string; title: string;

View file

@ -3,6 +3,8 @@ import "../styles/global.css";
//@ts-ignore //@ts-ignore
import Satellite from "../components/pages/Satellite"; import Satellite from "../components/pages/Satellite";
import { getSatelliteData, getSatelliteSlugs } from "../lib/satellite"; import { getSatelliteData, getSatelliteSlugs } from "../lib/satellite";
import UnderConstruction from "../components/pages/UnderConstruction";
import type { SatelliteContent } from "../lib/satellite";
export const prerender = false; export const prerender = false;
const { slug } = Astro.params; const { slug } = Astro.params;
@ -48,6 +50,7 @@ const satelliteData = await getSatelliteData(slug);
</head> </head>
<body> <body>
<Satellite client:only="react" slug={slug} content={satelliteData.data} /> {!satelliteData?.active && <UnderConstruction client:only="react" event_name={slug} record_id={satelliteData?.recordId} />}
{satelliteData?.active && <Satellite client:only="react" slug={slug} content={satelliteData.data as SatelliteContent} />}
</body> </body>
</html> </html>

View file

@ -1,6 +1,10 @@
--- ---
import "../styles/global.css"; import "../styles/global.css";
import App from "../components/pages/App" import App from "../components/pages/App"
import { loadEventsLoc } from "../lib/events";
const events = await loadEventsLoc();
--- ---
<!doctype html> <!doctype html>
@ -35,6 +39,6 @@ import App from "../components/pages/App"
</head> </head>
<body> <body>
<App client:only="react" /> <App client:only="react" events={events} />
</body> </body>
</html> </html>

27
astro/src/pages/map.astro Normal file
View file

@ -0,0 +1,27 @@
---
import "../styles/global.css";
import { Map } from "../components/primitives/Map";
import { loadEventsLoc } from "../lib/events";
const events = await loadEventsLoc();
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Map - Hack Club Campfire</title>
<!-- OpenGraph meta tags -->
<meta property="og:title" content="Campfire Map" />
<meta property="og:description" content="Find Campfire events happening around the world!" />
<meta property="og:image" content="/og-banner.png" />
<meta property="og:url" content="https://campfire.hackclub.com/map" />
<meta property="og:type" content="website" />
</head>
<body class="w-screen h-screen">
<Map client:only="react" events={events} className="w-full h-full" />
</body>
</html>

66
astro/src/styles/map.css Normal file
View file

@ -0,0 +1,66 @@
.leaflet-container {
background: linear-gradient(135deg, #CCF4FD 0%, #B8D9F8 100%) !important;
border-radius: 16px !important;
overflow: hidden !important;
}
.leaflet-control-zoom a {
background: #44DBC8 !important;
border: 2px solid #3CC2AF !important;
color: white !important;
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(68, 219, 200, 0.3) !important;
transition: all 0.2s ease-in-out !important;
}
.leaflet-control-zoom a:hover {
background: #3CC2AF !important;
transform: translateY(-1px) !important;
box-shadow: 0 6px 16px rgba(68, 219, 200, 0.4) !important;
}
.leaflet-control-attribution {
background: rgba(252, 247, 196, 0.9) !important;
border: 1px solid #D3B180 !important;
border-radius: 8px !important;
color: #78531D !important;
font-family: 'Atkinson Hyperlegible', system-ui, sans-serif !important;
backdrop-filter: blur(8px) !important;
display: none !important;
}
.leaflet-control-attribution a {
color: #4477A3 !important;
text-decoration: none !important;
}
.leaflet-control-attribution a:hover {
color: #44DBC8 !important;
}
.custom-popup .leaflet-popup-content-wrapper {
background: #FFFBDF !important;
border: 1px solid #D3B180 !important;
border-radius: 8px !important;
box-shadow: 0 4px 8px rgba(211, 177, 128, 0.15) !important;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
}
.custom-popup .leaflet-popup-content {
margin: 8px 12px !important;
color: #78531D !important;
font-size: 14px !important;
font-weight: 500 !important;
line-height: 1.2 !important;
}
.custom-popup .leaflet-popup-tip {
background: #FFFBDF !important;
border: 1px solid #D3B180 !important;
border-top: none !important;
border-right: none !important;
}
.leaflet-tile-pane {
filter: hue-rotate(5deg) saturate(0.8) brightness(1.1) !important;
}

View file

@ -0,0 +1,12 @@
/*
Warnings:
- A unique constraint covering the columns `[recordId]` on the table `Satellite` will be added. If there are existing duplicate values, this will fail.
- Added the required column `recordId` to the `Satellite` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Satellite" ADD COLUMN "recordId" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Satellite_recordId_key" ON "Satellite"("recordId");

View file

@ -15,6 +15,7 @@ datasource db {
model Satellite { model Satellite {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
recordId String @unique
slug String @unique slug String @unique
data Json data Json
active Boolean @default(true) active Boolean @default(true)

View file

@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
"clientVersion": "7.2.0", "clientVersion": "7.2.0",
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3", "engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
"activeProvider": "postgresql", "activeProvider": "postgresql",
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Satellite {\n id Int @id @default(autoincrement())\n slug String @unique\n data Json\n active Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n", "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?\n// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel Satellite {\n id Int @id @default(autoincrement())\n recordId String @unique\n slug String @unique\n data Json\n active Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
"runtimeDataModel": { "runtimeDataModel": {
"models": {}, "models": {},
"enums": {}, "enums": {},
@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = {
} }
} }
config.runtimeDataModel = JSON.parse("{\"models\":{\"Satellite\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"slug\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") config.runtimeDataModel = JSON.parse("{\"models\":{\"Satellite\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"recordId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"slug\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> { async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
const { Buffer } = await import('node:buffer') const { Buffer } = await import('node:buffer')

View file

@ -519,6 +519,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
export const SatelliteScalarFieldEnum = { export const SatelliteScalarFieldEnum = {
id: 'id', id: 'id',
recordId: 'recordId',
slug: 'slug', slug: 'slug',
data: 'data', data: 'data',
active: 'active', active: 'active',

View file

@ -72,6 +72,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
export const SatelliteScalarFieldEnum = { export const SatelliteScalarFieldEnum = {
id: 'id', id: 'id',
recordId: 'recordId',
slug: 'slug', slug: 'slug',
data: 'data', data: 'data',
active: 'active', active: 'active',

View file

@ -36,6 +36,7 @@ export type SatelliteSumAggregateOutputType = {
export type SatelliteMinAggregateOutputType = { export type SatelliteMinAggregateOutputType = {
id: number | null id: number | null
recordId: string | null
slug: string | null slug: string | null
active: boolean | null active: boolean | null
createdAt: Date | null createdAt: Date | null
@ -44,6 +45,7 @@ export type SatelliteMinAggregateOutputType = {
export type SatelliteMaxAggregateOutputType = { export type SatelliteMaxAggregateOutputType = {
id: number | null id: number | null
recordId: string | null
slug: string | null slug: string | null
active: boolean | null active: boolean | null
createdAt: Date | null createdAt: Date | null
@ -52,6 +54,7 @@ export type SatelliteMaxAggregateOutputType = {
export type SatelliteCountAggregateOutputType = { export type SatelliteCountAggregateOutputType = {
id: number id: number
recordId: number
slug: number slug: number
data: number data: number
active: number active: number
@ -71,6 +74,7 @@ export type SatelliteSumAggregateInputType = {
export type SatelliteMinAggregateInputType = { export type SatelliteMinAggregateInputType = {
id?: true id?: true
recordId?: true
slug?: true slug?: true
active?: true active?: true
createdAt?: true createdAt?: true
@ -79,6 +83,7 @@ export type SatelliteMinAggregateInputType = {
export type SatelliteMaxAggregateInputType = { export type SatelliteMaxAggregateInputType = {
id?: true id?: true
recordId?: true
slug?: true slug?: true
active?: true active?: true
createdAt?: true createdAt?: true
@ -87,6 +92,7 @@ export type SatelliteMaxAggregateInputType = {
export type SatelliteCountAggregateInputType = { export type SatelliteCountAggregateInputType = {
id?: true id?: true
recordId?: true
slug?: true slug?: true
data?: true data?: true
active?: true active?: true
@ -183,6 +189,7 @@ export type SatelliteGroupByArgs<ExtArgs extends runtime.Types.Extensions.Intern
export type SatelliteGroupByOutputType = { export type SatelliteGroupByOutputType = {
id: number id: number
recordId: string
slug: string slug: string
data: runtime.JsonValue data: runtime.JsonValue
active: boolean active: boolean
@ -215,6 +222,7 @@ export type SatelliteWhereInput = {
OR?: Prisma.SatelliteWhereInput[] OR?: Prisma.SatelliteWhereInput[]
NOT?: Prisma.SatelliteWhereInput | Prisma.SatelliteWhereInput[] NOT?: Prisma.SatelliteWhereInput | Prisma.SatelliteWhereInput[]
id?: Prisma.IntFilter<"Satellite"> | number id?: Prisma.IntFilter<"Satellite"> | number
recordId?: Prisma.StringFilter<"Satellite"> | string
slug?: Prisma.StringFilter<"Satellite"> | string slug?: Prisma.StringFilter<"Satellite"> | string
data?: Prisma.JsonFilter<"Satellite"> data?: Prisma.JsonFilter<"Satellite">
active?: Prisma.BoolFilter<"Satellite"> | boolean active?: Prisma.BoolFilter<"Satellite"> | boolean
@ -224,6 +232,7 @@ export type SatelliteWhereInput = {
export type SatelliteOrderByWithRelationInput = { export type SatelliteOrderByWithRelationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
recordId?: Prisma.SortOrder
slug?: Prisma.SortOrder slug?: Prisma.SortOrder
data?: Prisma.SortOrder data?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
@ -233,6 +242,7 @@ export type SatelliteOrderByWithRelationInput = {
export type SatelliteWhereUniqueInput = Prisma.AtLeast<{ export type SatelliteWhereUniqueInput = Prisma.AtLeast<{
id?: number id?: number
recordId?: string
slug?: string slug?: string
AND?: Prisma.SatelliteWhereInput | Prisma.SatelliteWhereInput[] AND?: Prisma.SatelliteWhereInput | Prisma.SatelliteWhereInput[]
OR?: Prisma.SatelliteWhereInput[] OR?: Prisma.SatelliteWhereInput[]
@ -241,10 +251,11 @@ export type SatelliteWhereUniqueInput = Prisma.AtLeast<{
active?: Prisma.BoolFilter<"Satellite"> | boolean active?: Prisma.BoolFilter<"Satellite"> | boolean
createdAt?: Prisma.DateTimeFilter<"Satellite"> | Date | string createdAt?: Prisma.DateTimeFilter<"Satellite"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Satellite"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Satellite"> | Date | string
}, "id" | "slug"> }, "id" | "recordId" | "slug">
export type SatelliteOrderByWithAggregationInput = { export type SatelliteOrderByWithAggregationInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
recordId?: Prisma.SortOrder
slug?: Prisma.SortOrder slug?: Prisma.SortOrder
data?: Prisma.SortOrder data?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
@ -262,6 +273,7 @@ export type SatelliteScalarWhereWithAggregatesInput = {
OR?: Prisma.SatelliteScalarWhereWithAggregatesInput[] OR?: Prisma.SatelliteScalarWhereWithAggregatesInput[]
NOT?: Prisma.SatelliteScalarWhereWithAggregatesInput | Prisma.SatelliteScalarWhereWithAggregatesInput[] NOT?: Prisma.SatelliteScalarWhereWithAggregatesInput | Prisma.SatelliteScalarWhereWithAggregatesInput[]
id?: Prisma.IntWithAggregatesFilter<"Satellite"> | number id?: Prisma.IntWithAggregatesFilter<"Satellite"> | number
recordId?: Prisma.StringWithAggregatesFilter<"Satellite"> | string
slug?: Prisma.StringWithAggregatesFilter<"Satellite"> | string slug?: Prisma.StringWithAggregatesFilter<"Satellite"> | string
data?: Prisma.JsonWithAggregatesFilter<"Satellite"> data?: Prisma.JsonWithAggregatesFilter<"Satellite">
active?: Prisma.BoolWithAggregatesFilter<"Satellite"> | boolean active?: Prisma.BoolWithAggregatesFilter<"Satellite"> | boolean
@ -270,6 +282,7 @@ export type SatelliteScalarWhereWithAggregatesInput = {
} }
export type SatelliteCreateInput = { export type SatelliteCreateInput = {
recordId: string
slug: string slug: string
data: Prisma.JsonNullValueInput | runtime.InputJsonValue data: Prisma.JsonNullValueInput | runtime.InputJsonValue
active?: boolean active?: boolean
@ -279,6 +292,7 @@ export type SatelliteCreateInput = {
export type SatelliteUncheckedCreateInput = { export type SatelliteUncheckedCreateInput = {
id?: number id?: number
recordId: string
slug: string slug: string
data: Prisma.JsonNullValueInput | runtime.InputJsonValue data: Prisma.JsonNullValueInput | runtime.InputJsonValue
active?: boolean active?: boolean
@ -287,6 +301,7 @@ export type SatelliteUncheckedCreateInput = {
} }
export type SatelliteUpdateInput = { export type SatelliteUpdateInput = {
recordId?: Prisma.StringFieldUpdateOperationsInput | string
slug?: Prisma.StringFieldUpdateOperationsInput | string slug?: Prisma.StringFieldUpdateOperationsInput | string
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
@ -296,6 +311,7 @@ export type SatelliteUpdateInput = {
export type SatelliteUncheckedUpdateInput = { export type SatelliteUncheckedUpdateInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
recordId?: Prisma.StringFieldUpdateOperationsInput | string
slug?: Prisma.StringFieldUpdateOperationsInput | string slug?: Prisma.StringFieldUpdateOperationsInput | string
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
@ -305,6 +321,7 @@ export type SatelliteUncheckedUpdateInput = {
export type SatelliteCreateManyInput = { export type SatelliteCreateManyInput = {
id?: number id?: number
recordId: string
slug: string slug: string
data: Prisma.JsonNullValueInput | runtime.InputJsonValue data: Prisma.JsonNullValueInput | runtime.InputJsonValue
active?: boolean active?: boolean
@ -313,6 +330,7 @@ export type SatelliteCreateManyInput = {
} }
export type SatelliteUpdateManyMutationInput = { export type SatelliteUpdateManyMutationInput = {
recordId?: Prisma.StringFieldUpdateOperationsInput | string
slug?: Prisma.StringFieldUpdateOperationsInput | string slug?: Prisma.StringFieldUpdateOperationsInput | string
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
@ -322,6 +340,7 @@ export type SatelliteUpdateManyMutationInput = {
export type SatelliteUncheckedUpdateManyInput = { export type SatelliteUncheckedUpdateManyInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number id?: Prisma.IntFieldUpdateOperationsInput | number
recordId?: Prisma.StringFieldUpdateOperationsInput | string
slug?: Prisma.StringFieldUpdateOperationsInput | string slug?: Prisma.StringFieldUpdateOperationsInput | string
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
active?: Prisma.BoolFieldUpdateOperationsInput | boolean active?: Prisma.BoolFieldUpdateOperationsInput | boolean
@ -331,6 +350,7 @@ export type SatelliteUncheckedUpdateManyInput = {
export type SatelliteCountOrderByAggregateInput = { export type SatelliteCountOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
recordId?: Prisma.SortOrder
slug?: Prisma.SortOrder slug?: Prisma.SortOrder
data?: Prisma.SortOrder data?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
@ -344,6 +364,7 @@ export type SatelliteAvgOrderByAggregateInput = {
export type SatelliteMaxOrderByAggregateInput = { export type SatelliteMaxOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
recordId?: Prisma.SortOrder
slug?: Prisma.SortOrder slug?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
@ -352,6 +373,7 @@ export type SatelliteMaxOrderByAggregateInput = {
export type SatelliteMinOrderByAggregateInput = { export type SatelliteMinOrderByAggregateInput = {
id?: Prisma.SortOrder id?: Prisma.SortOrder
recordId?: Prisma.SortOrder
slug?: Prisma.SortOrder slug?: Prisma.SortOrder
active?: Prisma.SortOrder active?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
@ -386,6 +408,7 @@ export type IntFieldUpdateOperationsInput = {
export type SatelliteSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type SatelliteSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
recordId?: boolean
slug?: boolean slug?: boolean
data?: boolean data?: boolean
active?: boolean active?: boolean
@ -395,6 +418,7 @@ export type SatelliteSelect<ExtArgs extends runtime.Types.Extensions.InternalArg
export type SatelliteSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type SatelliteSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
recordId?: boolean
slug?: boolean slug?: boolean
data?: boolean data?: boolean
active?: boolean active?: boolean
@ -404,6 +428,7 @@ export type SatelliteSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Ext
export type SatelliteSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{ export type SatelliteSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean id?: boolean
recordId?: boolean
slug?: boolean slug?: boolean
data?: boolean data?: boolean
active?: boolean active?: boolean
@ -413,6 +438,7 @@ export type SatelliteSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Ext
export type SatelliteSelectScalar = { export type SatelliteSelectScalar = {
id?: boolean id?: boolean
recordId?: boolean
slug?: boolean slug?: boolean
data?: boolean data?: boolean
active?: boolean active?: boolean
@ -420,13 +446,14 @@ export type SatelliteSelectScalar = {
updatedAt?: boolean updatedAt?: boolean
} }
export type SatelliteOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "slug" | "data" | "active" | "createdAt" | "updatedAt", ExtArgs["result"]["satellite"]> export type SatelliteOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "recordId" | "slug" | "data" | "active" | "createdAt" | "updatedAt", ExtArgs["result"]["satellite"]>
export type $SatellitePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = { export type $SatellitePayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Satellite" name: "Satellite"
objects: {} objects: {}
scalars: runtime.Types.Extensions.GetPayloadResult<{ scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number id: number
recordId: string
slug: string slug: string
data: runtime.JsonValue data: runtime.JsonValue
active: boolean active: boolean
@ -856,6 +883,7 @@ export interface Prisma__SatelliteClient<T, Null = never, ExtArgs extends runtim
*/ */
export interface SatelliteFieldRefs { export interface SatelliteFieldRefs {
readonly id: Prisma.FieldRef<"Satellite", 'Int'> readonly id: Prisma.FieldRef<"Satellite", 'Int'>
readonly recordId: Prisma.FieldRef<"Satellite", 'String'>
readonly slug: Prisma.FieldRef<"Satellite", 'String'> readonly slug: Prisma.FieldRef<"Satellite", 'String'>
readonly data: Prisma.FieldRef<"Satellite", 'Json'> readonly data: Prisma.FieldRef<"Satellite", 'Json'>
readonly active: Prisma.FieldRef<"Satellite", 'Boolean'> readonly active: Prisma.FieldRef<"Satellite", 'Boolean'>

View file

@ -7,12 +7,12 @@ Airtable.configure({
endpointUrl: 'https://api.airtable.com', endpointUrl: 'https://api.airtable.com',
}); });
const base = Airtable.base(process.env.AIRTABLE_BASE_ID!); const base = Airtable.base(process.env.AIRTABLE_BASE_ID!);
const eventsTable = base('Event'); const eventsTable = base('events');
export async function listOfEventWebsiteData() { export async function listOfEventWebsiteData() {
return (await eventsTable.select({ return (await eventsTable.select({
view: 'Everything', view: 'Everything',
filterByFormula: '{website_active} = 1', // filterByFormula: '{website_active} = 1',
fields: [ fields: [
'slug', //string 'slug', //string
'website_json', //string 'website_json', //string
@ -38,8 +38,8 @@ class AirtableSyncWorker {
for (const record of records) { for (const record of records) {
const slug = record.get('slug') as string; const slug = record.get('slug') as string;
const websiteJson = record.get('website_json') as string; const websiteJson = record.get('website_json') as string;
const websiteActive = record.get('website_active') as boolean; const websiteActive = record.get('website_active') === true;
if (!slug) { if (!slug) {
console.warn(`Skipping record ${record.id}: missing slug`); console.warn(`Skipping record ${record.id}: missing slug`);
continue; continue;
@ -56,18 +56,20 @@ class AirtableSyncWorker {
await prisma.satellite.upsert({ await prisma.satellite.upsert({
where: { slug }, where: { slug },
update: { update: {
recordId: record.id,
data, data,
active: websiteActive, active: websiteActive,
updatedAt: new Date(), updatedAt: new Date(),
}, },
create: { create: {
recordId: record.id,
slug, slug,
data, data,
active: websiteActive, active: websiteActive,
}, },
}); });
console.log(`✓ Synced: ${slug}`); console.log(`✓ Synced: ${slug} - Active: ${websiteActive}`);
} }
const inactiveCount = await prisma.satellite.updateMany({ const inactiveCount = await prisma.satellite.updateMany({