mirror of
https://github.com/System-End/campfire.git
synced 2026-04-19 16:28:22 +00:00
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:
parent
f4d6684426
commit
ae4b1e8519
24 changed files with 626 additions and 39 deletions
50
astro/package-lock.json
generated
50
astro/package-lock.json
generated
|
|
@ -15,13 +15,16 @@
|
|||
"@prisma/client": "^7.1.0",
|
||||
"@prisma/config": "^7.1.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^5.16.5",
|
||||
"better-auth": "^1.4.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"svelte": "^5.45.10",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3"
|
||||
|
|
@ -1415,6 +1418,17 @@
|
|||
"@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": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"license": "MIT"
|
||||
|
|
@ -2148,6 +2162,12 @@
|
|||
"@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": {
|
||||
"version": "3.0.4",
|
||||
"license": "MIT",
|
||||
|
|
@ -2155,6 +2175,15 @@
|
|||
"@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": {
|
||||
"version": "4.0.4",
|
||||
"license": "MIT",
|
||||
|
|
@ -3879,6 +3908,13 @@
|
|||
"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": {
|
||||
"version": "1.30.2",
|
||||
"license": "MPL-2.0",
|
||||
|
|
@ -5381,6 +5417,20 @@
|
|||
"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": {
|
||||
"version": "0.17.0",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -17,13 +17,16 @@
|
|||
"@prisma/client": "^7.1.0",
|
||||
"@prisma/config": "^7.1.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^5.16.5",
|
||||
"better-auth": "^1.4.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"svelte": "^5.45.10",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
@ -15,6 +15,7 @@ datasource db {
|
|||
|
||||
model Satellite {
|
||||
id Int @id @default(autoincrement())
|
||||
recordId String @unique
|
||||
slug String @unique
|
||||
data Json
|
||||
active Boolean @default(true)
|
||||
|
|
|
|||
BIN
astro/public/map/map-flag.png
Normal file
BIN
astro/public/map/map-flag.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 831 B |
BIN
astro/public/map/map-screenshot.png
Normal file
BIN
astro/public/map/map-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
|
|
@ -5,9 +5,11 @@ import Step from '../primitives/Step.js';
|
|||
import StoryCard from '../primitives/StoryCard.js';
|
||||
import GameCard from '../primitives/GameCard.js';
|
||||
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 clsx from 'clsx';
|
||||
import type { EventLocation } from '../../lib/events.ts';
|
||||
|
||||
const FORM_URL_ORGANIZER_APPLICATION = "https://forms.hackclub.com/t/8L51MzWyrHus";
|
||||
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 [scrollY, setScrollY] = useState(document.body.scrollTop);
|
||||
const [isLargeScreen, setIsLargeScreen] = useState(window.innerWidth >= 1280);
|
||||
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const [isMapOpen, setIsMapOpen] = useState(false);
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -111,7 +114,7 @@ function App() {
|
|||
/>
|
||||
</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">
|
||||
<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>
|
||||
|
|
@ -124,8 +127,8 @@ function App() {
|
|||
</header>
|
||||
|
||||
<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",
|
||||
windowHeight > windowWidth && windowWidth < 860 ? "flex items-stretch pb-16" : "flex items-end pb-32 2xl:pb-48"
|
||||
"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-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">
|
||||
<img src="/backgrounds/sky-shine.webp" alt="" className="w-full h-full object-cover select-none" />
|
||||
|
|
@ -159,21 +162,24 @@ function App() {
|
|||
/>
|
||||
</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
|
||||
src="/decorative/vines.webp"
|
||||
alt=""
|
||||
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(
|
||||
"flex flex-col gap-4 w-full md:w-[648px]",
|
||||
windowHeight > windowWidth && windowWidth < 860 && "justify-between h-full"
|
||||
)}>
|
||||
<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">
|
||||
<a href='https://hackclub.com' className='transition-transform hover:scale-105 active:scale-95'>
|
||||
|
|
@ -280,7 +286,7 @@ function App() {
|
|||
</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>
|
||||
</section>
|
||||
|
||||
|
|
@ -297,8 +303,8 @@ function App() {
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<VideoEmbed className="px-6" />
|
||||
<div className="xl:hidden pt-16 pb-8 relative z-50">
|
||||
<MapEmbed className="px-6 relative z-50 max-w-sm mx-auto" onOpenMap={() => setIsMapOpen(true)} />
|
||||
</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>
|
||||
|
|
@ -709,6 +715,29 @@ function App() {
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import FaqButton from '../primitives/FaqButton.js';
|
|||
import Step from '../primitives/Step.js';
|
||||
import GameCard from '../primitives/GameCard.js';
|
||||
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 clsx from 'clsx';
|
||||
import type { SatelliteContent } from '../../lib/satellite.ts';
|
||||
|
|
|
|||
153
astro/src/components/pages/UnderConstruction.tsx
Normal file
153
astro/src/components/pages/UnderConstruction.tsx
Normal 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;
|
||||
89
astro/src/components/primitives/Map.tsx
Normal file
89
astro/src/components/primitives/Map.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
astro/src/components/primitives/MapEmbed.tsx
Normal file
40
astro/src/components/primitives/MapEmbed.tsx
Normal 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
80
astro/src/lib/events.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
hero: {
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import "../styles/global.css";
|
|||
//@ts-ignore
|
||||
import Satellite from "../components/pages/Satellite";
|
||||
import { getSatelliteData, getSatelliteSlugs } from "../lib/satellite";
|
||||
import UnderConstruction from "../components/pages/UnderConstruction";
|
||||
import type { SatelliteContent } from "../lib/satellite";
|
||||
|
||||
export const prerender = false;
|
||||
const { slug } = Astro.params;
|
||||
|
|
@ -48,6 +50,7 @@ const satelliteData = await getSatelliteData(slug);
|
|||
|
||||
</head>
|
||||
<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>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
---
|
||||
import "../styles/global.css";
|
||||
import App from "../components/pages/App"
|
||||
|
||||
import { loadEventsLoc } from "../lib/events";
|
||||
|
||||
const events = await loadEventsLoc();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
|
|
@ -35,6 +39,6 @@ import App from "../components/pages/App"
|
|||
|
||||
</head>
|
||||
<body>
|
||||
<App client:only="react" />
|
||||
<App client:only="react" events={events} />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
27
astro/src/pages/map.astro
Normal file
27
astro/src/pages/map.astro
Normal 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
66
astro/src/styles/map.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
@ -15,6 +15,7 @@ datasource db {
|
|||
|
||||
model Satellite {
|
||||
id Int @id @default(autoincrement())
|
||||
recordId String @unique
|
||||
slug String @unique
|
||||
data Json
|
||||
active Boolean @default(true)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
|
|||
"clientVersion": "7.2.0",
|
||||
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
|
||||
"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": {
|
||||
"models": {},
|
||||
"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> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
|
|
|
|||
|
|
@ -519,6 +519,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
|
|||
|
||||
export const SatelliteScalarFieldEnum = {
|
||||
id: 'id',
|
||||
recordId: 'recordId',
|
||||
slug: 'slug',
|
||||
data: 'data',
|
||||
active: 'active',
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
|
|||
|
||||
export const SatelliteScalarFieldEnum = {
|
||||
id: 'id',
|
||||
recordId: 'recordId',
|
||||
slug: 'slug',
|
||||
data: 'data',
|
||||
active: 'active',
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export type SatelliteSumAggregateOutputType = {
|
|||
|
||||
export type SatelliteMinAggregateOutputType = {
|
||||
id: number | null
|
||||
recordId: string | null
|
||||
slug: string | null
|
||||
active: boolean | null
|
||||
createdAt: Date | null
|
||||
|
|
@ -44,6 +45,7 @@ export type SatelliteMinAggregateOutputType = {
|
|||
|
||||
export type SatelliteMaxAggregateOutputType = {
|
||||
id: number | null
|
||||
recordId: string | null
|
||||
slug: string | null
|
||||
active: boolean | null
|
||||
createdAt: Date | null
|
||||
|
|
@ -52,6 +54,7 @@ export type SatelliteMaxAggregateOutputType = {
|
|||
|
||||
export type SatelliteCountAggregateOutputType = {
|
||||
id: number
|
||||
recordId: number
|
||||
slug: number
|
||||
data: number
|
||||
active: number
|
||||
|
|
@ -71,6 +74,7 @@ export type SatelliteSumAggregateInputType = {
|
|||
|
||||
export type SatelliteMinAggregateInputType = {
|
||||
id?: true
|
||||
recordId?: true
|
||||
slug?: true
|
||||
active?: true
|
||||
createdAt?: true
|
||||
|
|
@ -79,6 +83,7 @@ export type SatelliteMinAggregateInputType = {
|
|||
|
||||
export type SatelliteMaxAggregateInputType = {
|
||||
id?: true
|
||||
recordId?: true
|
||||
slug?: true
|
||||
active?: true
|
||||
createdAt?: true
|
||||
|
|
@ -87,6 +92,7 @@ export type SatelliteMaxAggregateInputType = {
|
|||
|
||||
export type SatelliteCountAggregateInputType = {
|
||||
id?: true
|
||||
recordId?: true
|
||||
slug?: true
|
||||
data?: true
|
||||
active?: true
|
||||
|
|
@ -183,6 +189,7 @@ export type SatelliteGroupByArgs<ExtArgs extends runtime.Types.Extensions.Intern
|
|||
|
||||
export type SatelliteGroupByOutputType = {
|
||||
id: number
|
||||
recordId: string
|
||||
slug: string
|
||||
data: runtime.JsonValue
|
||||
active: boolean
|
||||
|
|
@ -215,6 +222,7 @@ export type SatelliteWhereInput = {
|
|||
OR?: Prisma.SatelliteWhereInput[]
|
||||
NOT?: Prisma.SatelliteWhereInput | Prisma.SatelliteWhereInput[]
|
||||
id?: Prisma.IntFilter<"Satellite"> | number
|
||||
recordId?: Prisma.StringFilter<"Satellite"> | string
|
||||
slug?: Prisma.StringFilter<"Satellite"> | string
|
||||
data?: Prisma.JsonFilter<"Satellite">
|
||||
active?: Prisma.BoolFilter<"Satellite"> | boolean
|
||||
|
|
@ -224,6 +232,7 @@ export type SatelliteWhereInput = {
|
|||
|
||||
export type SatelliteOrderByWithRelationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
recordId?: Prisma.SortOrder
|
||||
slug?: Prisma.SortOrder
|
||||
data?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
|
|
@ -233,6 +242,7 @@ export type SatelliteOrderByWithRelationInput = {
|
|||
|
||||
export type SatelliteWhereUniqueInput = Prisma.AtLeast<{
|
||||
id?: number
|
||||
recordId?: string
|
||||
slug?: string
|
||||
AND?: Prisma.SatelliteWhereInput | Prisma.SatelliteWhereInput[]
|
||||
OR?: Prisma.SatelliteWhereInput[]
|
||||
|
|
@ -241,10 +251,11 @@ export type SatelliteWhereUniqueInput = Prisma.AtLeast<{
|
|||
active?: Prisma.BoolFilter<"Satellite"> | boolean
|
||||
createdAt?: Prisma.DateTimeFilter<"Satellite"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"Satellite"> | Date | string
|
||||
}, "id" | "slug">
|
||||
}, "id" | "recordId" | "slug">
|
||||
|
||||
export type SatelliteOrderByWithAggregationInput = {
|
||||
id?: Prisma.SortOrder
|
||||
recordId?: Prisma.SortOrder
|
||||
slug?: Prisma.SortOrder
|
||||
data?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
|
|
@ -262,6 +273,7 @@ export type SatelliteScalarWhereWithAggregatesInput = {
|
|||
OR?: Prisma.SatelliteScalarWhereWithAggregatesInput[]
|
||||
NOT?: Prisma.SatelliteScalarWhereWithAggregatesInput | Prisma.SatelliteScalarWhereWithAggregatesInput[]
|
||||
id?: Prisma.IntWithAggregatesFilter<"Satellite"> | number
|
||||
recordId?: Prisma.StringWithAggregatesFilter<"Satellite"> | string
|
||||
slug?: Prisma.StringWithAggregatesFilter<"Satellite"> | string
|
||||
data?: Prisma.JsonWithAggregatesFilter<"Satellite">
|
||||
active?: Prisma.BoolWithAggregatesFilter<"Satellite"> | boolean
|
||||
|
|
@ -270,6 +282,7 @@ export type SatelliteScalarWhereWithAggregatesInput = {
|
|||
}
|
||||
|
||||
export type SatelliteCreateInput = {
|
||||
recordId: string
|
||||
slug: string
|
||||
data: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
||||
active?: boolean
|
||||
|
|
@ -279,6 +292,7 @@ export type SatelliteCreateInput = {
|
|||
|
||||
export type SatelliteUncheckedCreateInput = {
|
||||
id?: number
|
||||
recordId: string
|
||||
slug: string
|
||||
data: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
||||
active?: boolean
|
||||
|
|
@ -287,6 +301,7 @@ export type SatelliteUncheckedCreateInput = {
|
|||
}
|
||||
|
||||
export type SatelliteUpdateInput = {
|
||||
recordId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
|
|
@ -296,6 +311,7 @@ export type SatelliteUpdateInput = {
|
|||
|
||||
export type SatelliteUncheckedUpdateInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
recordId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
|
|
@ -305,6 +321,7 @@ export type SatelliteUncheckedUpdateInput = {
|
|||
|
||||
export type SatelliteCreateManyInput = {
|
||||
id?: number
|
||||
recordId: string
|
||||
slug: string
|
||||
data: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
||||
active?: boolean
|
||||
|
|
@ -313,6 +330,7 @@ export type SatelliteCreateManyInput = {
|
|||
}
|
||||
|
||||
export type SatelliteUpdateManyMutationInput = {
|
||||
recordId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
|
|
@ -322,6 +340,7 @@ export type SatelliteUpdateManyMutationInput = {
|
|||
|
||||
export type SatelliteUncheckedUpdateManyInput = {
|
||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
recordId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
slug?: Prisma.StringFieldUpdateOperationsInput | string
|
||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
||||
active?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||
|
|
@ -331,6 +350,7 @@ export type SatelliteUncheckedUpdateManyInput = {
|
|||
|
||||
export type SatelliteCountOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
recordId?: Prisma.SortOrder
|
||||
slug?: Prisma.SortOrder
|
||||
data?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
|
|
@ -344,6 +364,7 @@ export type SatelliteAvgOrderByAggregateInput = {
|
|||
|
||||
export type SatelliteMaxOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
recordId?: Prisma.SortOrder
|
||||
slug?: Prisma.SortOrder
|
||||
active?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
|
|
@ -352,6 +373,7 @@ export type SatelliteMaxOrderByAggregateInput = {
|
|||
|
||||
export type SatelliteMinOrderByAggregateInput = {
|
||||
id?: Prisma.SortOrder
|
||||
recordId?: Prisma.SortOrder
|
||||
slug?: Prisma.SortOrder
|
||||
active?: 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<{
|
||||
id?: boolean
|
||||
recordId?: boolean
|
||||
slug?: boolean
|
||||
data?: 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<{
|
||||
id?: boolean
|
||||
recordId?: boolean
|
||||
slug?: boolean
|
||||
data?: 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<{
|
||||
id?: boolean
|
||||
recordId?: boolean
|
||||
slug?: boolean
|
||||
data?: boolean
|
||||
active?: boolean
|
||||
|
|
@ -413,6 +438,7 @@ export type SatelliteSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Ext
|
|||
|
||||
export type SatelliteSelectScalar = {
|
||||
id?: boolean
|
||||
recordId?: boolean
|
||||
slug?: boolean
|
||||
data?: boolean
|
||||
active?: boolean
|
||||
|
|
@ -420,13 +446,14 @@ export type SatelliteSelectScalar = {
|
|||
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> = {
|
||||
name: "Satellite"
|
||||
objects: {}
|
||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||
id: number
|
||||
recordId: string
|
||||
slug: string
|
||||
data: runtime.JsonValue
|
||||
active: boolean
|
||||
|
|
@ -856,6 +883,7 @@ export interface Prisma__SatelliteClient<T, Null = never, ExtArgs extends runtim
|
|||
*/
|
||||
export interface SatelliteFieldRefs {
|
||||
readonly id: Prisma.FieldRef<"Satellite", 'Int'>
|
||||
readonly recordId: Prisma.FieldRef<"Satellite", 'String'>
|
||||
readonly slug: Prisma.FieldRef<"Satellite", 'String'>
|
||||
readonly data: Prisma.FieldRef<"Satellite", 'Json'>
|
||||
readonly active: Prisma.FieldRef<"Satellite", 'Boolean'>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ Airtable.configure({
|
|||
endpointUrl: 'https://api.airtable.com',
|
||||
});
|
||||
const base = Airtable.base(process.env.AIRTABLE_BASE_ID!);
|
||||
const eventsTable = base('Event');
|
||||
const eventsTable = base('events');
|
||||
|
||||
export async function listOfEventWebsiteData() {
|
||||
return (await eventsTable.select({
|
||||
view: 'Everything',
|
||||
filterByFormula: '{website_active} = 1',
|
||||
// filterByFormula: '{website_active} = 1',
|
||||
fields: [
|
||||
'slug', //string
|
||||
'website_json', //string
|
||||
|
|
@ -38,8 +38,8 @@ class AirtableSyncWorker {
|
|||
for (const record of records) {
|
||||
const slug = record.get('slug') 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) {
|
||||
console.warn(`Skipping record ${record.id}: missing slug`);
|
||||
continue;
|
||||
|
|
@ -56,18 +56,20 @@ class AirtableSyncWorker {
|
|||
await prisma.satellite.upsert({
|
||||
where: { slug },
|
||||
update: {
|
||||
recordId: record.id,
|
||||
data,
|
||||
active: websiteActive,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
recordId: record.id,
|
||||
slug,
|
||||
data,
|
||||
active: websiteActive,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✓ Synced: ${slug}`);
|
||||
console.log(`✓ Synced: ${slug} - Active: ${websiteActive}`);
|
||||
}
|
||||
|
||||
const inactiveCount = await prisma.satellite.updateMany({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue