diff --git a/astro.config.mjs b/astro.config.mjs index 86440638..5795cd1f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -91,6 +91,7 @@ export default defineConfig({ "/events-new/", "/home-new/", "/endorsements-new/", + "/e4p/pledge-new/", ]; return !exclude.some((path) => page.endsWith(path)); }, diff --git a/src/components/PledgeForm.tsx b/src/components/PledgeForm.tsx new file mode 100644 index 00000000..3f0e64df --- /dev/null +++ b/src/components/PledgeForm.tsx @@ -0,0 +1,194 @@ +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import TextField from "@mui/material/TextField"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Button from "@mui/material/Button"; +import Alert from "@mui/material/Alert"; +import CircularProgress from "@mui/material/CircularProgress"; +import Box from "@mui/material/Box"; + +type FormData = { + name: string; + email: string; + company: string; + position: string; + linkedin: string; + agreement: boolean; +}; + +const inputSx = { + backgroundColor: "white", + borderRadius: "8px", + "& .MuiInputBase-input": { + backgroundColor: "white", + }, +}; + +export default function PledgeForm() { + const [submitted, setSubmitted] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + defaultValues: { agreement: false }, + }); + + const onSubmit = async (data: FormData) => { + setSubmitting(true); + setError(null); + + try { + const body = new FormData(); + body.set("name", data.name); + body.set("email", data.email); + body.set("company", data.company); + body.set("position", data.position); + body.set("linkedin", data.linkedin); + if (data.agreement) body.set("agreement", "on"); + + const response = await fetch("/api/e4p-pledge-sign", { + method: "POST", + body, + }); + + if (!response.ok) { + const body = await response.json(); + throw new Error(body.error || "Submission failed"); + } + + reset(); + setSubmitted(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Something went wrong. Please try again."); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ + + + Name * + + } + fullWidth + sx={inputSx} + error={!!errors.name} + helperText={errors.name?.message} + {...register("name", { required: "Name is required" })} + /> + + Email * + + } + type="email" + fullWidth + sx={inputSx} + error={!!errors.email} + helperText={errors.email?.message} + {...register("email", { + required: "Email is required", + pattern: { value: /^\S+@\S+\.\S+$/, message: "Invalid email address" }, + })} + /> + + + + + Your company * + + } + fullWidth + sx={inputSx} + error={!!errors.company} + helperText={errors.company?.message} + {...register("company", { required: "Company is required" })} + /> + + Your position * + + } + fullWidth + sx={inputSx} + error={!!errors.position} + helperText={errors.position?.message} + {...register("position", { required: "Position is required" })} + /> + + + + LinkedIn URL * + + } + type="url" + fullWidth + placeholder="https://linkedin.com/in/your-profile" + sx={inputSx} + error={!!errors.linkedin} + helperText={errors.linkedin?.message} + {...register("linkedin", { + required: "LinkedIn URL is required", + pattern: { + value: /^https?:\/\/.+/, + message: "Must be a valid URL starting with http:// or https://", + }, + })} + /> + + } + label="I agree that my submitted data will be processed and stored by Entrepreneurs for Palestine and that my name, position and company will be listed under the Signatories section of this pledge." + /> + {errors.agreement && ( + + You must agree before signing the pledge. + + )} + + {error && {error}} + + + + {submitted && ( + + Thank you for signing the pledge. We will include you in the list of + signatories once your submission has been verified. + + )} + + +
+ ); +} diff --git a/src/components/SignatoriesNew.tsx b/src/components/SignatoriesNew.tsx new file mode 100644 index 00000000..8cd677a8 --- /dev/null +++ b/src/components/SignatoriesNew.tsx @@ -0,0 +1,108 @@ +import { useState, useEffect } from "react"; + +interface Signatory { + id: string; + name: string; + company: string; + position: string; + linkedinUrl: string; + signedAt: string; + approved: boolean; +} + +interface SignatoriesNewProps { + initialSignatories?: Signatory[]; + loading?: boolean; +} + +export default function SignatoriesNew({ + initialSignatories = [], + loading: initialLoading = false, +}: SignatoriesNewProps) { + const [signatories, setSignatories] = useState(initialSignatories); + const [loading, setLoading] = useState(initialLoading); + const [error, setError] = useState(null); + + const fetchSignatories = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch("/api/e4p-signatories"); + if (!response.ok) { + throw new Error("Failed to fetch signatories"); + } + + const data = await response.json(); + setSignatories(data); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch signatories"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (initialSignatories.length === 0) { + fetchSignatories(); + } + }, []); + + if (loading) { + return ( +
+ {[...Array(5)].map((_, i) => ( +
+
+
+ ))} +
+ ); + } + + if (error) { + return ( +
+

{error}

+ +
+ ); + } + + if (signatories.length === 0) { + return ( +
+

+ No signatories yet. Be the first to sign the pledge! +

+
+ ); + } + + return ( +
+

+ {signatories.length} {signatories.length === 1 ? "signatory" : "signatories"} +

+ +
    + {signatories.map((signatory) => ( +
  • + {signatory.name} + + {signatory.position} at {signatory.company} + +
  • + ))} +
+
+ ); +} diff --git a/src/layouts/HomeLayout.astro b/src/layouts/HomeLayout.astro index 0782c5ee..4e0e2333 100644 --- a/src/layouts/HomeLayout.astro +++ b/src/layouts/HomeLayout.astro @@ -49,6 +49,10 @@ const organizationSchema = { "https://github.com/TechForPalestine/", "https://www.linkedin.com/company/techforpalestine/", "https://infosec.exchange/@tech4palestine", + "https://www.youtube.com/@tech4palestine", + "https://techforpalestine.org/discord-invite", + "https://www.tiktok.com/@techforpalestine", + "https://bsky.app/profile/techforpalestine.org", ], }; --- diff --git a/src/pages/about-new.astro b/src/pages/about-new.astro index 38bb3914..5240883f 100644 --- a/src/pages/about-new.astro +++ b/src/pages/about-new.astro @@ -6,6 +6,18 @@ import Button from "../components/ui/Button.astro"; import SignUpForm from "../structures/SignUpForm.astro"; const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; + +const founderSchema = { + "@context": "https://schema.org", + "@type": "Person", + name: "Paul Biggar", + jobTitle: "Founder", + worksFor: { + "@type": "NonprofitOrganization", + name: "Tech for Palestine", + }, + sameAs: ["https://blog.paulbiggar.com/", "https://circleci.com", "https://darklang.com"], +}; --- + + diff --git a/src/pages/get-involved-new.astro b/src/pages/get-involved-new.astro index 3c4f6908..43b1023d 100644 --- a/src/pages/get-involved-new.astro +++ b/src/pages/get-involved-new.astro @@ -9,7 +9,7 @@ const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true"; --- diff --git a/src/pages/help/hire-new.astro b/src/pages/help/hire-new.astro index 78b20729..5d1eb55e 100644 --- a/src/pages/help/hire-new.astro +++ b/src/pages/help/hire-new.astro @@ -154,8 +154,8 @@ const hireItemListSchema = { --- diff --git a/src/pages/ideas-new.astro b/src/pages/ideas-new.astro index 1aa88b46..4ec4b3d1 100644 --- a/src/pages/ideas-new.astro +++ b/src/pages/ideas-new.astro @@ -31,7 +31,7 @@ const startedIdeas = rawIdeas.filter((i) => i.category === "started").map(mapIde --- diff --git a/src/pages/media-new.astro b/src/pages/media-new.astro index db9ce7e4..be724bd2 100644 --- a/src/pages/media-new.astro +++ b/src/pages/media-new.astro @@ -159,7 +159,7 @@ const pressSchema = { ---