Skip to content

Commit 2100370

Browse files
committed
Sign in via email, multiple post types, ui changes & updates
1 parent dce20f8 commit 2100370

43 files changed

Lines changed: 1334 additions & 609 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/(app)/cities/[id]/page.tsx

Lines changed: 0 additions & 48 deletions
This file was deleted.

app/(app)/cities/page.tsx

Lines changed: 0 additions & 48 deletions
This file was deleted.

app/(app)/companies/[id]/page.tsx

Lines changed: 0 additions & 53 deletions
This file was deleted.

app/(app)/companies/page.tsx

Lines changed: 0 additions & 43 deletions
This file was deleted.

app/(app)/directory/page.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
"use client";
22

33
import { useEffect, useState } from "react";
4-
import { UserProfile, Company } from "@/types";
4+
import { UserProfile, Company, City } from "@/types";
55
import { DirectoryFilters } from "@/components/directory/DirectoryFilters";
66
import { AlumniCard } from "@/components/directory/AlumniCard";
77

88
export default function DirectoryPage() {
99
const [allUsers, setAllUsers] = useState<UserProfile[]>([]);
1010
const [companies, setCompanies] = useState<Company[]>([]);
11+
const [cities, setCities] = useState<City[]>([]);
1112
const [nameFilter, setNameFilter] = useState("");
1213
const [classYearFilter, setClassYearFilter] = useState("");
14+
const [cityFilter, setCityFilter] = useState("");
15+
const [companyFilter, setCompanyFilter] = useState("");
1316

1417
useEffect(() => {
1518
Promise.all([
1619
fetch("/api/users").then((r) => r.json()),
1720
fetch("/api/companies").then((r) => r.json()),
18-
]).then(([users, comps]) => {
21+
fetch("/api/cities").then((r) => r.json()),
22+
]).then(([users, comps, cityList]) => {
1923
setAllUsers(users);
2024
setCompanies(comps);
25+
setCities(cityList);
2126
});
2227
}, []);
2328

@@ -26,7 +31,9 @@ export default function DirectoryPage() {
2631
!nameFilter ||
2732
`${u.firstName} ${u.lastName}`.toLowerCase().includes(nameFilter.toLowerCase());
2833
const matchYear = !classYearFilter || u.classYear === parseInt(classYearFilter);
29-
return matchName && matchYear;
34+
const matchCity = !cityFilter || u.cityId === cityFilter;
35+
const matchCompany = !companyFilter || u.companyIds.includes(companyFilter);
36+
return matchName && matchYear && matchCity && matchCompany;
3037
});
3138

3239
return (
@@ -35,8 +42,14 @@ export default function DirectoryPage() {
3542
<DirectoryFilters
3643
name={nameFilter}
3744
classYear={classYearFilter}
45+
cityId={cityFilter}
46+
companyId={companyFilter}
47+
cities={cities}
48+
companies={companies}
3849
onNameChange={setNameFilter}
3950
onClassYearChange={setClassYearFilter}
51+
onCityChange={setCityFilter}
52+
onCompanyChange={setCompanyFilter}
4053
/>
4154
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
4255
{filtered.map((u) => (

app/(app)/feed/page.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,29 @@
33
import { useState } from "react";
44
import { PostList } from "@/components/feed/PostList";
55
import { CreatePostDialog } from "@/components/feed/CreatePostDialog";
6+
import { AnnouncementsSection } from "@/components/feed/AnnouncementsSection";
7+
import { EventsSection } from "@/components/feed/EventsSection";
68

79
export default function FeedPage() {
810
const [refreshKey, setRefreshKey] = useState(0);
11+
const refresh = () => setRefreshKey((k) => k + 1);
912

1013
return (
11-
<div className="space-y-6">
12-
<div className="flex items-center justify-between">
13-
<h1 className="text-2xl font-semibold">Posts</h1>
14-
<CreatePostDialog onSuccess={() => setRefreshKey((k) => k + 1)} />
15-
</div>
16-
<PostList refreshKey={refreshKey} />
14+
<div className="space-y-8">
15+
<AnnouncementsSection refreshKey={refreshKey} />
16+
<EventsSection refreshKey={refreshKey} />
17+
18+
<section className="space-y-4">
19+
<div className="flex items-center justify-between">
20+
<h1 className="text-2xl font-semibold">Posts</h1>
21+
<CreatePostDialog
22+
onSuccess={refresh}
23+
allowedTypes={["post", "announcement", "event"]}
24+
defaultType="post"
25+
/>
26+
</div>
27+
<PostList refreshKey={refreshKey} type={["post", "joined"]} />
28+
</section>
1729
</div>
1830
);
1931
}

app/(app)/jobs/page.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { PostList } from "@/components/feed/PostList";
5+
import { CreatePostDialog } from "@/components/feed/CreatePostDialog";
6+
7+
export default function JobsPage() {
8+
const [refreshKey, setRefreshKey] = useState(0);
9+
10+
return (
11+
<div className="space-y-6">
12+
<div className="flex items-center justify-between">
13+
<h1 className="text-2xl font-semibold">Jobs</h1>
14+
<CreatePostDialog
15+
onSuccess={() => setRefreshKey((k) => k + 1)}
16+
allowedTypes={["job"]}
17+
defaultType="job"
18+
label="Post a job"
19+
/>
20+
</div>
21+
<PostList refreshKey={refreshKey} type="job" />
22+
</div>
23+
);
24+
}

app/(auth)/login/verify/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Suspense } from "react";
2+
import { VerifyForm } from "@/components/auth/VerifyForm";
3+
4+
export default function VerifyPage() {
5+
return (
6+
<Suspense>
7+
<VerifyForm />
8+
</Suspense>
9+
);
10+
}

app/(auth)/signup/page.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import { SignupForm } from "@/components/auth/SignupForm";
1+
import { redirect } from "next/navigation";
22

3-
export default async function SignupPage({
4-
searchParams,
5-
}: {
6-
searchParams: Promise<{ code?: string }>;
7-
}) {
8-
const { code } = await searchParams;
9-
return <SignupForm code={code} />;
3+
export default function SignupPage() {
4+
redirect("/login");
105
}

app/api/auth/send-link/route.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { adminAuth, adminDb } from "@/lib/firebase/admin";
3+
import { Resend } from "resend";
4+
import { SUPER_ADMIN_EMAIL } from "@/lib/constants";
5+
6+
const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
7+
const isDev = process.env.NODE_ENV === "development";
8+
9+
export async function POST(request: NextRequest) {
10+
try {
11+
const { email } = await request.json();
12+
if (!email) return NextResponse.json({ error: "Email required" }, { status: 400 });
13+
14+
if (email !== SUPER_ADMIN_EMAIL) {
15+
// Allow existing members or those with a valid (unused) invitation
16+
const [userSnap, invSnap] = await Promise.all([
17+
adminDb.collection("users").where("email", "==", email).where("profileComplete", "==", true).get(),
18+
adminDb.collection("invitations").where("email", "==", email).get(),
19+
]);
20+
21+
const hasAccount = !userSnap.empty;
22+
const hasValidInvitation = !invSnap.empty && !invSnap.docs[0].data().usedAt;
23+
24+
if (!hasAccount && !hasValidInvitation) {
25+
return NextResponse.json(
26+
{ error: "No account found for this email." },
27+
{ status: 404 }
28+
);
29+
}
30+
}
31+
32+
const link = await adminAuth.generateSignInWithEmailLink(email, {
33+
url: `${APP_URL}/login/verify`,
34+
handleCodeInApp: true,
35+
});
36+
37+
if (isDev) {
38+
console.log("\n📬 [DEV] Magic sign-in link (not sent)");
39+
console.log(` To: ${email}`);
40+
console.log(` Link: ${link}\n`);
41+
} else {
42+
const resend = new Resend(process.env.RESEND_API_KEY);
43+
await resend.emails.send({
44+
from: "AppDev Alumni <noreply@alumni.cornellappdev.com>",
45+
to: email,
46+
subject: "Your sign-in link for Cornell AppDev Alumni",
47+
html: `<p>Hi,</p><p>Click the link below to sign in. This link expires in 1 hour and can only be used once.</p><p><a href="${link}">Sign in to AppDev Alumni</a></p><p>If you didn't request this, you can ignore this email.</p>`,
48+
});
49+
}
50+
51+
return NextResponse.json({ ok: true });
52+
} catch (error) {
53+
console.error("Send link error:", error);
54+
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
55+
}
56+
}

0 commit comments

Comments
 (0)