Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default defineConfig({
"/events-new/",
"/home-new/",
"/endorsements-new/",
"/e4p/pledge-new/",
];
return !exclude.some((path) => page.endsWith(path));
},
Expand Down
194 changes: 194 additions & 0 deletions src/components/PledgeForm.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null);

const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<FormData>({
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 (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" }, gap: 3 }}>
<TextField
label={
<span>
Name <span style={{ color: "#AB4956" }}>*</span>
</span>
}
fullWidth
sx={inputSx}
error={!!errors.name}
helperText={errors.name?.message}
{...register("name", { required: "Name is required" })}
/>
<TextField
label={
<span>
Email <span style={{ color: "#AB4956" }}>*</span>
</span>
}
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" },
})}
/>
</Box>

<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" }, gap: 3 }}>
<TextField
label={
<span>
Your company <span style={{ color: "#AB4956" }}>*</span>
</span>
}
fullWidth
sx={inputSx}
error={!!errors.company}
helperText={errors.company?.message}
{...register("company", { required: "Company is required" })}
/>
<TextField
label={
<span>
Your position <span style={{ color: "#AB4956" }}>*</span>
</span>
}
fullWidth
sx={inputSx}
error={!!errors.position}
helperText={errors.position?.message}
{...register("position", { required: "Position is required" })}
/>
</Box>

<TextField
label={
<span>
LinkedIn URL <span style={{ color: "#AB4956" }}>*</span>
</span>
}
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://",
},
})}
/>

<FormControlLabel
control={<Checkbox {...register("agreement", { required: true })} />}
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 && (
<Alert severity="error" sx={{ mt: -2 }}>
You must agree before signing the pledge.
</Alert>
)}

{error && <Alert severity="error">{error}</Alert>}

<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<Button
type="submit"
variant="contained"
disabled={submitting}
startIcon={submitting ? <CircularProgress size={16} color="inherit" /> : null}
sx={{
backgroundColor: "#AB4956",
"&:hover": { backgroundColor: "#D35464" },
px: 4,
py: 1.5,
fontWeight: 600,
}}
>
{submitting ? "Signing..." : "Sign pledge"}
</Button>
{submitted && (
<Alert severity="success">
<strong>Thank you for signing the pledge.</strong> We will include you in the list of
signatories once your submission has been verified.
</Alert>
)}
</Box>
</Box>
</form>
);
}
108 changes: 108 additions & 0 deletions src/components/SignatoriesNew.tsx
Original file line number Diff line number Diff line change
@@ -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<Signatory[]>(initialSignatories);
const [loading, setLoading] = useState(initialLoading);
const [error, setError] = useState<string | null>(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 (
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="animate-pulse">
<div className="h-4 w-3/4 rounded bg-butter"></div>
</div>
))}
</div>
);
}

if (error) {
return (
<div className="py-8 text-center">
<p className="ts-body-large mb-4 text-brand">{error}</p>
<button
onClick={fetchSignatories}
className="ts-label rounded-pill bg-brand px-5 py-3 text-white transition-colors hover:bg-brand-hover"
>
Try Again
</button>
</div>
);
}

if (signatories.length === 0) {
return (
<div className="py-8">
<p className="ts-body-large text-ink-secondary">
No signatories yet. Be the first to sign the pledge!
</p>
</div>
);
}

return (
<div>
<p className="ts-label mb-6 text-ink-secondary">
{signatories.length} {signatories.length === 1 ? "signatory" : "signatories"}
</p>

<ul>
{signatories.map((signatory) => (
<li
key={signatory.id}
className="flex flex-wrap items-baseline gap-x-1.5 border-b border-butter py-3 last:border-b-0"
>
<span className="ts-label text-ink">{signatory.name}</span>
<span className="ts-body-small text-ink-secondary">
{signatory.position} at {signatory.company}
</span>
</li>
))}
</ul>
</div>
);
}
4 changes: 4 additions & 0 deletions src/layouts/HomeLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
};
---
Expand Down
15 changes: 15 additions & 0 deletions src/pages/about-new.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,28 @@ 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"],
};
---

<HomeLayout
title="About | Tech for Palestine"
description="Tech for Palestine is a 501(c)(3) nonprofit that incubates and scales tech and advocacy projects for Palestinian liberation."
noindex={true}
>
<Fragment slot="head">
<script type="application/ld+json" set:html={JSON.stringify(founderSchema)} />
</Fragment>
<HomeNavbar membershipLive={membershipLive} alwaysVisible />
<main class="pt-20">
<!-- Hero -->
Expand Down
2 changes: 1 addition & 1 deletion src/pages/contact-new.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const membershipLive = getEnv("MEMBERSHIP_LIVE", Astro.locals) === "true";
---

<HomeLayout
title="Contact Us"
title="Contact Us | Tech for Palestine"
description="Get in touch with Tech for Palestine — general questions, media inquiries, or request an endorsement for your campaign."
noindex={true}
>
Expand Down
6 changes: 3 additions & 3 deletions src/pages/e4p-new.astro
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const peopleSchema = [...testimonials, ...founders].map((person) => ({
---

<HomeLayout
title="Entrepreneurs for Palestine"
title="Entrepreneurs for Palestine | Tech for Palestine"
description="Entrepreneurs for Palestine is a global community of founders and CEOs joining forces to stand up for what's right and make a difference for the Palestinian cause."
noindex={true}
>
Expand Down Expand Up @@ -197,7 +197,7 @@ const peopleSchema = [...testimonials, ...founders].map((person) => ({
<Button href="/e4p/sign-up" variant="primary" size="md" arrow>
Join the Community
</Button>
<Button href="/e4p/pledge" variant="ghost" size="md"> Sign the E4P Pledge </Button>
<Button href="/e4p/pledge-new" variant="ghost" size="md"> Sign the E4P Pledge </Button>
</div>
</div>
</div>
Expand Down Expand Up @@ -278,7 +278,7 @@ const peopleSchema = [...testimonials, ...founders].map((person) => ({
>.&rdquo;
</blockquote>
<a
href="/e4p/pledge"
href="/e4p/pledge-new"
class="ts-label inline-flex min-h-[44px] items-center gap-2 rounded-pill border border-butter px-5 py-3.5 text-brand transition-colors duration-150 hover:bg-butter"
>
Sign the E4P Pledge
Expand Down
Loading
Loading