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
62 changes: 62 additions & 0 deletions events_extracted_updated.csv

Large diffs are not rendered by default.

817 changes: 817 additions & 0 deletions events_mongodb_import.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/app/api/send/event-new/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const POST = withApiAuth(

const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: `Spokes Events <${senderEmail}>`,
from: `Spokes Website <${senderEmail}>`,
to: [adminEmail],
subject: `${eventData.organization} submitted an event and is pending approval...`,
react: NewEvent({
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/send/event-reject/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const POST = withApiAuth(
}
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: `Spokes Events <${senderEmail}>`,
from: `Spokes Website <${senderEmail}>`,
to: [`${eventData.contactEmail}`],
subject: `Your event submission for ${eventData.eventName} was rejected`,
react: RejectEvent({
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/send/new/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const POST = withApiAuth(

const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: `Spokes Job Board <${senderEmail}>`,
from: `Spokes Website <${senderEmail}>`,
to: [adminEmail],
subject: `${jobData.organizationName} submitted a job and is pending approval...`,
react: NewJob({
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/send/reject/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const POST = withApiAuth(
}
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: `Spokes Job Board <${senderEmail}>`,
from: `Spokes Website <${senderEmail}>`,
to: [`${jobData.contactEmail}`],
subject: `Your job post for ${jobData.title} was rejected`,
react: RejectJob({
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/send/update/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const POST = withApiAuth(

const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: `Spokes Job Board <${senderEmail}>`,
from: `Spokes Website <${senderEmail}>`,
to: [adminEmail],
subject: `${jobData.organizationName} updated a job and is pending approval`,
react: UpdatedJob({
Expand Down
2 changes: 1 addition & 1 deletion src/app/eventsLogin/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import DashboardLoginPage from "@/components/auth/DashboardLoginPage";

export default function EventsLoginPage() {
return <DashboardLoginPage redirectUrl="/events" />;
return <DashboardLoginPage redirectUrl="/events" board="events" />;
}
2 changes: 1 addition & 1 deletion src/app/jobsLogin/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import DashboardLoginPage from "@/components/auth/DashboardLoginPage";

export default function JobsLoginPage() {
return <DashboardLoginPage redirectUrl="/jobs" />;
return <DashboardLoginPage redirectUrl="/jobs" board="jobs" />;
}
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import Footer from "@/components/Footer/Footer";
const inter = Inter({ subsets: ["latin"] });

export const metadata = {
title: "Spokes Job Board",
description: "The job board is a place for nonprofit organizations to post their job openings.",
title: "Spokes Job & Event Board",
description: "A nonprofit job and event board from Spokes.",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
Expand Down
16 changes: 16 additions & 0 deletions src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ const Footer: React.FC = () => {
)}
</div>

<div className="flex flex-col items-center md:items-start gap-[8px] md:gap-[10px] md:w-[135px] mt-4 md:mt-0 md:ml-[40px]">
<Link href="/events" className="text-[14px] md:text-[16px] text-gray-600 hover:underline">
Event Board
</Link>
{user && (
<>
<Link href="/events/list" className="text-[14px] md:text-[16px] text-gray-600 hover:underline">
List Event
</Link>
<Link href="/events/manage" className="text-[14px] md:text-[16px] text-gray-600 hover:underline">
Dashboard
</Link>
</>
)}
</div>

<div className="flex space-x-[10px] md:space-x-[14px] mt-4 md:mt-0 md:ml-[40px]">
<Link href="https://www.instagram.com/spokes.for.nonprofits/" target="_blank" rel="noopener noreferrer">
<FaInstagram width={30} height={30} className="w-[30px] h-[30px] md:w-[35px] md:h-[35px]" />
Expand Down
9 changes: 5 additions & 4 deletions src/components/Onboarding/OnboardingError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { Button, Stack, Alert, AlertIcon, AlertTitle, AlertDescription } from "@
interface OnboardingErrorProps {
error: string;
onRetry: () => void;
onReturnToJobBoard: () => void;
boardLabel: string;
onReturnToBoard: () => void;
}

export default function OnboardingError({ error, onRetry, onReturnToJobBoard }: OnboardingErrorProps) {
export default function OnboardingError({ error, onRetry, boardLabel, onReturnToBoard }: OnboardingErrorProps) {
return (
<Alert
status="error"
Expand All @@ -30,8 +31,8 @@ export default function OnboardingError({ error, onRetry, onReturnToJobBoard }:
<Button onClick={onRetry} colorScheme="blackAlpha" bg="black" _hover={{ bg: "#5E5E5E" }}>
Try Again
</Button>
<Button onClick={onReturnToJobBoard} variant="outline" colorScheme="blackAlpha" textColor="black">
Return to Job Board
<Button onClick={onReturnToBoard} variant="outline" colorScheme="blackAlpha" textColor="black">
Return to {boardLabel}
</Button>
</Stack>
</Alert>
Expand Down
18 changes: 13 additions & 5 deletions src/components/Onboarding/OnboardingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export default function OnboardingPage() {
const router = useRouter();
const searchParams = useSearchParams();
const returnUrl = searchParams.get("returnUrl") || "/";
const isEventReturn = returnUrl.startsWith("/events");
const boardLabel = isEventReturn ? "Event Board" : "Job Board";
const boardReturnUrl = isEventReturn ? "/events" : "/jobs";
const toast = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -71,11 +74,11 @@ export default function OnboardingPage() {
router.push(returnUrl);
} else {
const error = await response.json();
setError("We couldn't complete your profile at this time. Please try again or return to the job board.");
setError(`We couldn't complete your profile at this time. Please try again or return to the ${boardLabel}.`);
console.error("Failed to complete onboarding:", error);
}
} catch (error) {
setError("An unexpected error occurred. Please try again or return to the job board.");
setError(`An unexpected error occurred. Please try again or return to the ${boardLabel}.`);
console.error("Error during onboarding:", error);
} finally {
setIsSubmitting(false);
Expand All @@ -86,8 +89,8 @@ export default function OnboardingPage() {
setError(null);
};

const handleReturnToJobBoard = () => {
router.push("/");
const handleReturnToBoard = () => {
router.push(boardReturnUrl);
};

return (
Expand All @@ -107,7 +110,12 @@ export default function OnboardingPage() {
Complete Your Profile
</Heading>
{error ? (
<OnboardingError error={error} onRetry={handleRetry} onReturnToJobBoard={handleReturnToJobBoard} />
<OnboardingError
error={error}
onRetry={handleRetry}
boardLabel={boardLabel}
onReturnToBoard={handleReturnToBoard}
/>
) : (
<OnboardingForm onSubmit={handleSubmit} isSubmitting={isSubmitting} />
)}
Expand Down
10 changes: 8 additions & 2 deletions src/components/auth/DashboardLoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { Center } from "@chakra-ui/react";
import { usePathname } from "next/navigation";
import WelcomeInfo from "@/components/ui/WelcomeInfo";

export default function DashboardLoginPage({ redirectUrl }: { redirectUrl: string }) {
export default function DashboardLoginPage({
redirectUrl,
board = "jobs",
}: {
redirectUrl: string;
board?: "jobs" | "events";
}) {
const [isClient, setIsClient] = useState(false);
const pathname = usePathname();
const isAuthCallback = pathname.includes("sso-callback");
Expand All @@ -26,7 +32,7 @@ export default function DashboardLoginPage({ redirectUrl }: { redirectUrl: strin
gap={10}
minH="65vh"
>
{!isAuthCallback && <WelcomeInfo />}
{!isAuthCallback && <WelcomeInfo board={board} />}
<SignIn forceRedirectUrl={redirectUrl} />
</Center>
);
Expand Down
22 changes: 22 additions & 0 deletions src/components/ui/WelcomeInfo.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { render, screen } from "@testing-library/react";
import WelcomeInfo from "@/components/ui/WelcomeInfo";

describe("WelcomeInfo", () => {
it("keeps job-specific copy on the job board login", () => {
render(<WelcomeInfo board="jobs" />);

expect(screen.getByText(/To post a job/)).toBeInTheDocument();
expect(screen.getByText(/List Job/)).toBeInTheDocument();
expect(screen.queryByText(/To post an event/)).not.toBeInTheDocument();
});

it("uses event-specific copy on the event board login", () => {
render(<WelcomeInfo board="events" />);

expect(screen.getByText(/To post an event/)).toBeInTheDocument();
expect(screen.getByText(/List Event/)).toBeInTheDocument();
expect(screen.getByText(/Spokes event board/)).toBeInTheDocument();
expect(screen.queryByText(/To post a job/)).not.toBeInTheDocument();
expect(screen.queryByText(/List Job/)).not.toBeInTheDocument();
});
});
42 changes: 32 additions & 10 deletions src/components/ui/WelcomeInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,66 @@
"use client";
import React from "react";

export default function WelcomeInfo() {
type WelcomeInfoProps = {
board?: "jobs" | "events";
};

export default function WelcomeInfo({ board = "jobs" }: WelcomeInfoProps) {
const boardCopy =
board === "events"
? {
intro:
"To post an event, your nonprofit must have 501(c)(3) status. Once you sign in, you can post, manage, and edit events anytime.",
stepOne: "Confirm your nonprofit has 501(c)(3) status",
stepTwo: "Fill out the “List Event” form",
stepThree: "You'll get a notification once it's approved",
stepFour: "Your event will be live on the Spokes event board",
stepFive: "Manage events anytime in your dashboard",
}
: {
intro:
"To post a job, your nonprofit must have 501(c)(3) status. Once you sign in, you can post, manage, and edit listings anytime.",
stepOne: "Confirm your nonprofit has 501(c)(3) status",
stepTwo: "Fill out the “List Job” form",
stepThree: "You'll get a notification once it's approved",
stepFour: "Your job will be live for 30 days",
stepFive: "Manage listings anytime in your dashboard",
};

return (
<div className="max-w-md overflow-hidden p-6 ">
<h2 className="text-2xl font-bold mb-4">Welcome Nonprofit Admins!</h2>
<p className="pt-2 pb-10">
To post a job, your nonprofit must have 501(c)(3) status. Once you sign in, you can post, manage, and edit
listings anytime.
</p>
<p className="pt-2 pb-10">{boardCopy.intro}</p>
<ul className="space-y-3 relative list-none">
<li className="pl-6 relative p">
<span className="absolute left-0 top-1/2 transform -translate-y-1/2 w-6 h-6 rounded-full bg-orange-700 text-white flex items-center justify-center text-sm font-bold">
1
</span>
<p className="pl-2">Confirm your nonprofit has 501(c)(3) status</p>
<p className="pl-2">{boardCopy.stepOne}</p>
</li>
<li className="pl-6 relative">
<span className="absolute left-0 top-1/2 transform -translate-y-1/2 w-6 h-6 rounded-full bg-orange-700 text-white flex items-center justify-center text-sm font-bold">
2
</span>
<p className="pl-2">Fill out the “List Job” form</p>
<p className="pl-2">{boardCopy.stepTwo}</p>
</li>
<li className="pl-6 relative">
<span className="absolute left-0 top-1/2 transform -translate-y-1/2 w-6 h-6 rounded-full bg-orange-700 text-white flex items-center justify-center text-sm font-bold">
3
</span>
<p className="pl-2">You&#39;ll get a notification once it&#39;s approved</p>
<p className="pl-2">{boardCopy.stepThree}</p>
</li>
<li className="pl-6 relative">
<span className="absolute left-0 top-1/2 transform -translate-y-1/2 w-6 h-6 rounded-full bg-orange-700 text-white flex items-center justify-center text-sm font-bold">
4
</span>
<p className="pl-2">Your job will be live for 30 days</p>
<p className="pl-2">{boardCopy.stepFour}</p>
</li>
<li className="pl-6 relative">
<span className="absolute left-0 top-1/2 transform -translate-y-1/2 w-6 h-6 rounded-full bg-orange-700 text-white flex items-center justify-center text-sm font-bold">
5
</span>
<p className="pl-2">Manage listings anytime in your dashboard</p>
<p className="pl-2">{boardCopy.stepFive}</p>
</li>
</ul>
</div>
Expand Down
36 changes: 36 additions & 0 deletions src/lib/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getEventsAdminUrl, getJobsAdminUrl, getJobsListUrl } from "@/lib/url";

describe("admin URLs", () => {
const originalAdminUrl = process.env.NEXT_PUBLIC_ADMIN_URL;
const originalApiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;

afterEach(() => {
if (originalAdminUrl === undefined) {
delete process.env.NEXT_PUBLIC_ADMIN_URL;
} else {
process.env.NEXT_PUBLIC_ADMIN_URL = originalAdminUrl;
}

if (originalApiBaseUrl === undefined) {
delete process.env.NEXT_PUBLIC_API_BASE_URL;
} else {
process.env.NEXT_PUBLIC_API_BASE_URL = originalApiBaseUrl;
}
});

it("uses the configured host with the correct board path", () => {
process.env.NEXT_PUBLIC_ADMIN_URL = "https://board.example.com/admin";
delete process.env.NEXT_PUBLIC_API_BASE_URL;

expect(getJobsAdminUrl()).toBe("https://board.example.com/jobs/admin");
expect(getEventsAdminUrl()).toBe("https://board.example.com/events/admin");
expect(getJobsListUrl()).toBe("https://board.example.com/jobs/list");
});

it("falls back to the API base URL when no admin URL is configured", () => {
delete process.env.NEXT_PUBLIC_ADMIN_URL;
process.env.NEXT_PUBLIC_API_BASE_URL = "https://board.example.com";

expect(getEventsAdminUrl()).toBe("https://board.example.com/events/admin");
});
});
2 changes: 1 addition & 1 deletion src/lib/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ function getAdminUrl(path: string) {
const configuredUrl = process.env.NEXT_PUBLIC_ADMIN_URL?.trim();

if (configuredUrl) {
return configuredUrl;
return new URL(path, configuredUrl).toString();
}

const appUrl = process.env.NEXT_PUBLIC_API_BASE_URL?.trim();
Expand Down
Loading