diff --git a/README.md b/README.md index c91ec4a..e4fc5e9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # UPWayPoint + 📍 UP WayPoint is a map that utilizes the Google Maps API to display a map of the campus of UP Diliman. Users with a UP email may sign up and place pins on certain points of interest. Both UP and non-UP users can then use the map as a reference for all the notable spots within the campus, or as a means of navigation. _This is a course requirement for CS 191/CS192 Software Engineering Courses of the Department of Computer Science, College of Engineering, University of the Philippines, Diliman under the guidance of Prof. Ma. Rowena C. Solamo for the AY 2025-2026._ diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx index 8ac3308..31f034c 100644 --- a/apps/web/app/admin/page.tsx +++ b/apps/web/app/admin/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useMemo } from "react"; import { useRouter } from "next/navigation"; import { signOut } from "@/lib/auth-client"; import { trpc } from "@/lib/trpc"; @@ -9,169 +9,150 @@ import { useTheme } from "@/lib/ThemeContext"; import Link from "next/link"; export default function AdminDashboard() { - const router = useRouter(); - - const { data, isLoading } = trpc.user.getCurrent.useQuery(); - - const { data: pendingModifications } = - trpc.modification.getPending.useQuery(); - - const { data: pinCounts } = trpc.pin.getStatusCounts.useQuery(); - const { data: userCount } = trpc.user.getCount.useQuery(); - const { data: commentCount } = trpc.comment.getCount.useQuery(); - const { data: pendingPins } = trpc.pin.getAllAdmin.useQuery({ - status: "PENDING_VERIFICATION", - }); - - const { data: activePins } = trpc.pin.getAllAdmin.useQuery({ - status: "ACTIVE", - limit: 5, - }); - const utils = trpc.useUtils(); - const rejectPin = trpc.pin.reject.useMutation({ - onSuccess: (output) => { - utils.pin.getAllAdmin.invalidate(); - utils.modification.getPending.invalidate(); - }, - }); - const approvePin = trpc.pin.approve.useMutation({ - onSuccess: (output) => { - utils.pin.getAll.invalidate(); - utils.pin.getAllAdmin.invalidate(); - utils.modification.getPending.invalidate(); - }, - }); - const applyMod = trpc.pin.applyUpdate.useMutation({ - onSuccess: (output) => { - utils.modification.getPending.invalidate(); - utils.pin.getAll.invalidate(); - utils.pin.getAllAdmin.invalidate(); - utils.pin.getStatusCounts.invalidate(); - }, - }); - const rejectMod = trpc.pin.rejectUpdate.useMutation({ - onSuccess: (output) => { - utils.modification.getPending.invalidate(); - }, - }); - - const deletePin = trpc.pin.adminDelete.useMutation({ - onSuccess: (output) => { - utils.pin.getAllAdmin.invalidate(); - }, - }); - - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - const { theme, toggleTheme } = useTheme(); - const [activeSection, setActiveSection] = useState("overview"); - - const [isDeletingPin, setIsDeletingPin] = useState(false); - - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - setActiveSection(entry.target.id); - } - }); - }, - { - root: document.querySelector(".content-area"), - rootMargin: "-10% 0px -70% 0px", - }, - ); - - const sections = document.querySelectorAll(".dashboard-section"); - sections.forEach((section) => { - observer.observe(section); - }); - - return () => observer.disconnect(); - }, []); - - const scrollToSection = (sectionId: string) => { - const element = document.getElementById(sectionId); - if (element) { - element.scrollIntoView({ behavior: "smooth", block: "start" }); - setActiveSection(sectionId); - if (window.innerWidth <= 768) setIsSidebarOpen(false); - } - }; - - const goToMap = () => router.push("/"); - - const handleSignOut = async () => { - await signOut(); - router.refresh(); - }; - - const activePinCount = pinCounts?.ACTIVE || 0; - const rejectedPinCount = pinCounts?.ARCHIVED || 0; - const pendingPinCount = pinCounts?.PENDING_VERIFICATION || 0; - const totalPins = activePinCount + rejectedPinCount + pendingPinCount; - - const pinTagCounts = { - academic: 0, - food: 0, - transit: 0, - utility: 0, - social: 0, - }; - - const globalVerificationRate = useMemo( - () => Math.round((activePinCount / (totalPins || 1)) * 100) || 0, - [activePinCount, totalPins], - ); - - const globalUserStats = { - totalUsers: userCount, - totalComments: commentCount, - // avgPins: 3.6, - // avgComments: 5.3, - // newUsers7Days: 14, - // newUsers30Days: 45, - }; - - return ( -
- {/* --- MOBILE OVERLAY --- */} - {isSidebarOpen && ( - // biome-ignore lint/a11y/noStaticElementInteractions: - // biome-ignore lint/a11y/useKeyWithClickEvents: -
setIsSidebarOpen(false)} - /> - )} - - {/* --- SIDEBAR --- */} - - -
- {/* --- HEADER --- */} -
-
- -

- Admin Dashboard -

-
-
- - {/* --- MAIN --- */} -
-
-
-

- {isLoading - ? "LOADING..." - : `Welcome, ${data?.name ? data.name.toUpperCase() : "ADMIN"}!`} -

-

- You have accessed the restricted area. 🚨 -

-
- -
-
-

OVERVIEW

-
-
-
-

OVERALL PIN STATISTICS

-
-
- {/* Top Stats Grid */} -
-
- TOTAL PINS - {totalPins} -
-
- PENDING PINS - - {pendingPinCount} - -
-
- -
-
- - GLOBAL VERIFICATION - - - {globalVerificationRate}% - + +
+ +
+ + DISPLAY SETTINGS + + +
+ + +
+ +
+ + +
+ {/* --- HEADER --- */} +
+
+ +

+ Admin Dashboard +

+
+
+ + {/* --- MAIN --- */} +
+
+
+

+ {isLoading + ? "LOADING..." + : `Welcome, ${data?.name ? data.name.toUpperCase() : "ADMIN"}!`} +

+

+ You have accessed the restricted area. 🚨 +

+
+ +
+
+

+ OVERVIEW +

+
+
+
+

+ OVERALL PIN STATISTICS +

+
+
+
+
+ + TOTAL PINS + + + {totalPins} + +
+
+ + PENDING PINS + + + {pendingPinCount} + +
+
+ +
+
+ + GLOBAL VERIFICATION + + + {globalVerificationRate}% + +
+
+
+
+
+ + {activePinCount} VERIFIED + + + {pendingPinCount} PENDING + + + {rejectedPinCount} REJECTED + +
+
+ +
+
+ {PIN_CATEGORIES.map((category) => { + const count = + pinTagCounts[ + category.id as keyof typeof pinTagCounts + ] || 0; + const percentage = + totalPins > 0 + ? (count / (totalPins || 1)) * 100 + : 0; + + return ( +
+
+ + {category.label} + + + {count} + +
+
+
+
+
+ ); + })} +
+
+
+
+ +
+
+

+ OVERALL USER STATISTICS +

+
+
+
+
+ + TOTAL USERS + + + {globalUserStats.totalUsers} + +
+
+ + TOTAL COMMENTS + + + {globalUserStats.totalComments} + +
+ {/*
+ + AVERAGE PINS / USER + + + {globalUserStats.avgPins} +
-
-
-
-
- - {activePinCount} VERIFIED - - - {pendingPinCount} PENDING - - - {rejectedPinCount} REJECTED - -
-
- -
- CATEGORY BREAKDOWN -
- {PIN_CATEGORIES.map((category) => { - const count = - pinTagCounts[ - category.id as keyof typeof pinTagCounts - ] || 0; - const percentage = - totalPins > 0 - ? (count / (totalPins || 1)) * 100 - : 0; - - return ( -
-
- - {category.label} - - {count} -
-
-
-
-
- ); - })} -
-
-
-
- -
-
-

OVERALL USER STATISTICS

-
- -
-
-
- TOTAL USERS - - {globalUserStats.totalUsers} - -
-
- TOTAL COMMENTS - - {globalUserStats.totalComments} - -
- {/*
- - AVERAGE PINS / USER - - - {globalUserStats.avgPins} - -
-
- - AVERAGE COMMENTS / USER - - - {globalUserStats.avgComments} - -
-
- - NEW USERS FOR THE LAST WEEK - - - {globalUserStats.newUsers7Days} - -
-
- - NEW USERS FOR THE LAST MONTH - - - {globalUserStats.newUsers30Days} - -
*/} -
-
-
-
-
- -
-

PIN MANAGEMENT

-
-
-
-

PENDING PIN VERIFICATIONS

-
- -
-
- {pendingPins?.map((pin) => { - const color = getPinColor( - pin.pinTags?.[0]?.tag.title || "", - ); - return ( -
-
-
- - {pin.title.charAt(0).toUpperCase()} - -
- -
- {pin.title} - - By {pin.owner} • {pin.latitude.toFixed(4)},{" "} - {pin.longitude.toFixed(4)} - -
-
- -
- - - - - - - - - -
-
- ); - })} -
-
-
- -
-
-

RECENTLY VERIFIED PINS

-
- -
-
- {activePins?.map((pin) => { - const color = getPinColor( - pin.pinTags?.[0]?.tag.title || "", - ); - return ( -
-
-
- - {pin.title.charAt(0).toUpperCase()} - -
- -
- {pin.title} - - By {pin.owner} • {pin.latitude.toFixed(4)},{" "} - {pin.longitude.toFixed(4)} - -
-
- -
- - - - - - {isDeletingPin ? ( - <> - - - - - ) : ( - - )} -
-
- ); - })} -
-
-
- -
-
-

RECENT MODIFICATION REQUESTS

-
- -
-
- {pendingModifications?.map((mod) => { - const color = "var(--text-primary)"; - return ( -
-
-
- - {mod.pin.title.charAt(0).toUpperCase()} - -
- -
- - {mod.pin.title} - - - Modification by {mod.user.name} - -
-
- -
- - - - - - - - - -
-
- ); - })} -
-
-
-
-
- - {/*
-

USER MANAGEMENT

-
-
-
-

NEWEST USERS

+
+ + AVERAGE COMMENTS / USER + + + {globalUserStats.avgComments} + +
+
+ + NEW USERS FOR THE LAST WEEK + + + {globalUserStats.newUsers7Days} + +
+
+ + NEW USERS FOR THE LAST MONTH + + + {globalUserStats.newUsers30Days} + +
*/} +
+
+
+
+
+ +
+

+ PIN MANAGEMENT +

+
+
+
+

+ PENDING PIN VERIFICATIONS +

+
+ +
+
+ {pendingPins?.map((pin) => { + const color = getPinColor( + pin.pinTags?.[0]?.tag.title || "", + ); + return ( +
+
+
+ + {pin.title.charAt(0).toUpperCase()} + +
+ +
+ + {pin.title} + + + By {pin.owner} • {pin.latitude.toFixed(4)},{" "} + {pin.longitude.toFixed(4)} + +
+
+ +
+ + + + + + + + + +
+
+ ); + })} +
+
+
+ +
+
+

+ RECENTLY VERIFIED PINS +

+
+ +
+
+ {activePins?.map((pin) => { + const color = getPinColor( + pin.pinTags?.[0]?.tag.title || "", + ); + return ( +
+
+
+ + {pin.title.charAt(0).toUpperCase()} + +
+ +
+ + {pin.title} + + + By {pin.owner} • {pin.latitude.toFixed(4)},{" "} + {pin.longitude.toFixed(4)} + +
+
+ +
+ + + + + + {isDeletingPin ? ( + <> + + + + + ) : ( + + )} +
+
+ ); + })} +
+
+
+ +
+
+

+ RECENT MODIFICATION REQUESTS +

+
+ +
+
+ {pendingModifications?.map((mod) => { + const color = "var(--text-primary)"; + return ( +
+
+
+ + {mod.pin.title.charAt(0).toUpperCase()} + +
+ +
+ + {mod.pin.title} + + + Modification by {mod.user.name} + +
+
+ +
+ + + + + + + + + +
+
+ ); + })} +
+
+
+
+
+ + {/*
+

USER MANAGEMENT

+
+
+
+

NEWEST USERS

@@ -901,9 +897,9 @@ export default function AdminDashboard() {
-
-
-

TOP USERS BY PINS

+
+
+

TOP USERS BY PINS

@@ -958,643 +954,10 @@ export default function AdminDashboard() {
*/} -
-
-
-
- - -
- ); +
+ + + + + ); } diff --git a/apps/web/app/api/auth/[...all]/route.ts b/apps/web/app/api/auth/[...all]/route.ts index f872afe..cb41a80 100644 --- a/apps/web/app/api/auth/[...all]/route.ts +++ b/apps/web/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ import { auth } from "@repo/auth"; // path to your auth file import { toNextJsHandler } from "better-auth/next-js"; -export const { POST, GET } = toNextJsHandler(auth); \ No newline at end of file +export const { POST, GET } = toNextJsHandler(auth); diff --git a/apps/web/app/api/trpc/[trpc]/route.ts b/apps/web/app/api/trpc/[trpc]/route.ts index addce35..8cc55bc 100644 --- a/apps/web/app/api/trpc/[trpc]/route.ts +++ b/apps/web/app/api/trpc/[trpc]/route.ts @@ -3,11 +3,11 @@ import { createContext } from "@repo/api"; import { appRouter } from "@repo/api"; const handler = (req: Request) => - fetchRequestHandler({ - endpoint: "/api/trpc", - req, - router: appRouter, - createContext: () => createContext({ req }), - }); + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + createContext: () => createContext({ req }), + }); export { handler as GET, handler as POST }; diff --git a/apps/web/app/api/upload/route.ts b/apps/web/app/api/upload/route.ts index df22f4f..c5cd5c8 100644 --- a/apps/web/app/api/upload/route.ts +++ b/apps/web/app/api/upload/route.ts @@ -1,40 +1,40 @@ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { randomUUID } from "node:crypto"; const s3 = new S3Client({ - region: "auto", // Required by AWS SDK, not used by R2 - // Provide your R2 endpoint: https://.r2.cloudflarestorage.com - endpoint: process.env.S3_API, - credentials: { - accessKeyId: process.env.R2_ACCESS_KEY_ID!, - secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, - }, + region: "auto", // Required by AWS SDK, not used by R2 + // Provide your R2 endpoint: https://.r2.cloudflarestorage.com + endpoint: process.env.S3_API, + credentials: { + accessKeyId: process.env.R2_ACCESS_KEY_ID!, + secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, + }, }); export async function POST(req: Request) { - const formData = await req.formData(); - const files = formData.getAll("images") as File[]; + const formData = await req.formData(); + const files = formData.getAll("images") as File[]; - if (!files.length) { - return Response.json({ message: "No files provided" }, { status: 200 }); - } + if (!files.length) { + return Response.json({ message: "No files provided" }, { status: 200 }); + } - const urls = await Promise.all( - files.map(async (file) => { - const buffer = Buffer.from(await file.arrayBuffer()); - const key = `pins/${randomUUID()}-${file.name}`; + const urls = await Promise.all( + files.map(async (file) => { + const buffer = Buffer.from(await file.arrayBuffer()); + const key = `pins/${randomUUID()}-${file.name}`; - await s3.send( - new PutObjectCommand({ - Bucket: process.env.R2_BUCKET_NAME!, - Key: key, - Body: buffer, - ContentType: file.type, - }), - ); + await s3.send( + new PutObjectCommand({ + Bucket: process.env.R2_BUCKET_NAME!, + Key: key, + Body: buffer, + ContentType: file.type, + }), + ); - return `https://pub-6c943fcf0c9447139d523a8511236ae8.r2.dev/${key}`; - }), - ); + return `https://pub-6c943fcf0c9447139d523a8511236ae8.r2.dev/${key}`; + }), + ); - return Response.json({ urls }, { status: 200 }); + return Response.json({ urls }, { status: 200 }); } diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx index 44d6d64..f77df8b 100644 --- a/apps/web/app/dashboard/page.tsx +++ b/apps/web/app/dashboard/page.tsx @@ -9,293 +9,309 @@ import { useTheme } from "@/lib/ThemeContext"; import Link from "next/link"; export default function Dashboard() { - const router = useRouter(); - const { data, isLoading } = trpc.user.getCurrent.useQuery(); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - - const { data: userPins } = trpc.pin.getCurrentUsersPins.useQuery(undefined, { - refetchOnWindowFocus: false, - }); - - const { data: userComments } = trpc.comment.getCurrentUsersComments.useQuery( - undefined, - { - refetchOnWindowFocus: false, - }, - ); - - // const [isEditingBio, setIsEditingBio] = useState(false); - // const [bioInput, setBioInput] = useState(""); - const { theme, toggleTheme } = useTheme(); - - // const handleSaveBio = () => { - // // TODO: Hook this up to trpc.user.updateBio.useMutation() - // console.log("Saving new bio:", bioInput); - // setIsEditingBio(false); - // }; - - const goToMap = () => router.push("/"); - const goToAdmin = () => router.push("/admin"); - - const handleSignOut = async () => { - await signOut(); - router.refresh(); - }; - const stats = useMemo(() => { - return { - totalPins: userPins?.filter((p) => p.status !== "DELETED").length, - verifiedPins: userPins?.filter((p) => p.status === "ACTIVE").length, - pendingPins: userPins?.filter((p) => p.status === "PENDING_VERIFICATION") - .length, - rejectedPins: userPins?.filter((p) => p.status === "ARCHIVED").length, - comments: userComments?.length, - categoryBreakdown: { - academic: userPins?.filter( - (p) => - p.pinTags.map((pt) => pt.tag.title).includes("academic") && - p.status !== "DELETED", - ).length, - food: userPins?.filter( - (p) => - p.pinTags.map((pt) => pt.tag.title).includes("food") && - p.status !== "DELETED", - ).length, - social: userPins?.filter( - (p) => - p.pinTags.map((pt) => pt.tag.title).includes("social") && - p.status !== "DELETED", - ).length, - utility: userPins?.filter( - (p) => - p.pinTags.map((pt) => pt.tag.title).includes("utility") && - p.status !== "DELETED", - ).length, - transit: userPins?.filter( - (p) => - p.pinTags.map((pt) => pt.tag.title).includes("transit") && - p.status !== "DELETED", - ).length, - }, - pendingList: userPins - ?.filter((p) => p.status === "PENDING_VERIFICATION") - .slice(0, 5), - recentList: userPins?.filter((p) => p.status === "ACTIVE").slice(0, 5), - }; - }, [userPins, userComments]); - - const verificationRate = - stats.verifiedPins && stats.totalPins - ? Math.round((stats.verifiedPins / stats.totalPins) * 100) - : 0; - - return ( -
- {/* --- MOBILE OVERLAY --- */} - {isSidebarOpen && ( - // biome-ignore lint/a11y/noStaticElementInteractions: - // biome-ignore lint/a11y/useKeyWithClickEvents: -
setIsSidebarOpen(false)} - /> - )} - - {/* --- SIDEBAR --- */} - - -
- {/* --- HEADER --- */} -
-
- -

User Dashboard

-
-
- - {/* --- MAIN --- */} -
-
-
-

- {isLoading - ? "LOADING..." - : `Welcome, ${data?.name ? data.name.toUpperCase() : "UNKNOWN"}!`} -

-

You made it!

-
- - {/* --- DASHBOARD GRID --- */} -
-
-
-

YOUR PROFILE

- - {data?.userRole === "admin" ? "ADMIN" : "REGULAR USER"} - -
- -
-
-
-
- {data?.name ? data.name.charAt(0).toUpperCase() : "O"} -
-
-
- - {data?.name || "UNKNOWN NAME"} - - - {(data as any)?.email || "UNKNOWN EMAIL"} - -
-
- - {/*
+ > +
+ + + + + GO TO ADMIN DASHBOARD +
+ + )} + +
+
+ + DISPLAY SETTINGS + + + +
+ + +
+ +
+ + +
+ {/* --- HEADER --- */} +
+
+ +

+ User Dashboard +

+
+
+ + {/* --- MAIN --- */} +
+
+
+

+ {isLoading + ? "LOADING..." + : `Welcome, ${data?.name ? data.name.toUpperCase() : "UNKNOWN"}!`} +

+

+ You made it! +

+
+ + {/* --- DASHBOARD GRID --- */} +
+
+
+

+ YOUR PROFILE +

+ + {data?.userRole === "admin" ? "ADMIN" : "REGULAR USER"} + +
+ +
+
+
+
+ {data?.name ? data.name.charAt(0).toUpperCase() : "O"} +
+
+
+ + {data?.name || "UNKNOWN NAME"} + + + {(data as { email?: string })?.email || "UNKNOWN EMAIL"} + +
+
+ + {/*
BIO {!isEditingBio && ( @@ -356,980 +372,256 @@ export default function Dashboard() {

)}
*/} -
-
- -
-
-

YOUR STATISTICS

-
- -
- {/* Top Stats Grid */} -
-
- TOTAL PINS ADDED - {stats.totalPins} -
-
- TOTAL COMMENTS - {stats.comments} -
-
- - {/* Verification Integrity Bar */} -
-
- VERIFICATIONS - - {verificationRate}% - -
-
-
-
-
- - {stats.verifiedPins} VERIFIED - - - {stats.pendingPins} PENDING - - - {stats.rejectedPins} REJECTED - -
-
- - {/* Category Distribution */} -
- CATEGORY DISTRIBUTION -
- {PIN_CATEGORIES.map((category) => { - const count = - stats.categoryBreakdown[ - category.id as keyof typeof stats.categoryBreakdown - ] || 0; - const percentage = - stats.totalPins && stats.totalPins > 0 - ? (count / stats.totalPins) * 100 - : 0; - - return ( -
-
- - {category.label} - - {count} -
-
-
-
-
- ); - })} -
-
-
-
- -
-
-

YOUR PENDING PINS

- - {stats.pendingList?.length} - -
-
-
- {stats.pendingList?.map((pin) => { - const color = getPinColor( - pin.pinTags?.[0]?.tag.title || "", - ); - return ( -
-
-
- - {pin.title.charAt(0).toUpperCase()} - -
- -
- {pin.title} - - {pin.latitude.toFixed(4)},{" "} - {pin.longitude.toFixed(4)} - -
-
- - - - - - -
- ); - })} -
-
-
- -
-
-

YOUR RECENT PINS

- - {stats.recentList?.length} - -
-
-
- {stats.recentList?.map((pin) => { - const color = getPinColor( - pin.pinTags?.[0]?.tag.title || "", - ); - return ( -
-
-
- - {pin.title.charAt(0).toUpperCase()} - -
- -
- {pin.title} - - {pin.latitude.toFixed(4)},{" "} - {pin.longitude.toFixed(4)} - -
-
- - - - - - -
- ); - })} -
-
-
-
-
-
-
- - -
- ); +
+
+ +
+
+

+ YOUR STATISTICS +

+
+ +
+ {/* Top Stats Grid */} +
+
+ + TOTAL PINS ADDED + + + {stats.totalPins} + +
+
+ + TOTAL COMMENTS + + + {stats.comments} + +
+
+ + {/* Verification Integrity Bar */} +
+
+ + VERIFICATIONS + + + {verificationRate}% + +
+
+
+
+
+ + {stats.verifiedPins} VERIFIED + + + {stats.pendingPins} PENDING + + + {stats.rejectedPins} REJECTED + +
+
+ + {/* Category Distribution */} +
+ + CATEGORY DISTRIBUTION + +
+ {PIN_CATEGORIES.map((category) => { + const count = + stats.categoryBreakdown[ + category.id as keyof typeof stats.categoryBreakdown + ] || 0; + const percentage = + stats.totalPins && stats.totalPins > 0 + ? (count / stats.totalPins) * 100 + : 0; + + return ( +
+
+ + {category.label} + + + {count} + +
+
+
+
+
+ ); + })} +
+
+
+
+ +
+
+

+ YOUR PENDING PINS +

+ + {stats.pendingList?.length} + +
+
+
+ {stats.pendingList?.map((pin) => { + const color = getPinColor( + pin.pinTags?.[0]?.tag.title || "", + ); + return ( +
+
+
+ + {pin.title.charAt(0).toUpperCase()} + +
+ +
+ + {pin.title} + + + {pin.latitude.toFixed(4)},{" "} + {pin.longitude.toFixed(4)} + +
+
+ + + + + + +
+ ); + })} +
+
+
+ +
+
+

+ YOUR RECENT PINS +

+ + {stats.recentList?.length} + +
+
+
+ {stats.recentList?.map((pin) => { + const color = getPinColor( + pin.pinTags?.[0]?.tag.title || "", + ); + return ( +
+
+
+ + {pin.title.charAt(0).toUpperCase()} + +
+ +
+ + {pin.title} + + + {pin.latitude.toFixed(4)},{" "} + {pin.longitude.toFixed(4)} + +
+
+ + + + + + +
+ ); + })} +
+
+
+
+
+ +
+
+ ); } diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 3ebd735..5590593 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1,5 +1,43 @@ @import "tailwindcss"; +@theme { + --font-chakra: var(--font-chakra), sans-serif; + --font-nunito: var(--font-nunito), sans-serif; + --font-cubao-wide: var(--font-cubao-wide), sans-serif; + + --color-base: var(--bg-base); + --color-panel: var(--bg-panel); + --color-panel-hover: var(--bg-panel-hover); + + --color-primary: var(--text-primary); + --color-secondary: var(--text-secondary); + --color-border-color: var(--border-color); + + --color-neon-blue: var(--neon-blue); + --color-neon-red: var(--neon-red); + --color-neon-yellow: var(--neon-yellow); + + --color-status-success: var(--status-success); + --color-status-warning: var(--status-warning); + --color-status-danger: var(--status-danger); + + --color-pin-academic: var(--pin-academic); + --color-pin-food: var(--pin-food); + --color-pin-social: var(--pin-social); + --color-pin-transit: var(--pin-transit); + --color-pin-utility: var(--pin-utility); + + --animate-pan: autoPan 20s linear infinite; + --animate-scroll-text: scrollText 5s ease-in-out infinite alternate; + --animate-slide-up: slideUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; + --animate-fade-in: fadeIn 0.2s ease-out forwards; + --animate-fade-up: fadeUp 0.3s ease-out forwards; + --animate-pulse-glow: pulseGlow 2s infinite alternate; + --animate-radar: radarPing 2s cubic-bezier(0, 0, 0.2, 1) infinite; + --animate-slide-right: slideRight 0.3s ease-out forwards; + --animate-spin-slow: spin 20s linear infinite; +} + @keyframes spin { 100% { transform: rotate(360deg); } } @@ -87,10 +125,6 @@ from { transform: translateX(-100%); } to { transform: translateX(0); } } -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} :root { --bg-base: #f0f2f5; @@ -174,28 +208,11 @@ p { } .tactical-panel { - background: var(--bg-panel); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--border-color); - border-radius: 16px; - color: var(--text-primary); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); - transition: - background 0.3s ease, - border-color 0.3s ease, - color 0.3s ease; + @apply bg-panel backdrop-blur-md border border-border-color rounded-2xl text-primary shadow-[0_8px_32px_rgba(0,0,0,0.15)] transition-colors duration-300; } .tactical-button { - background: var(--bg-panel); - border: 1px solid var(--border-color); - color: var(--text-primary); - border-radius: 8px; - font-family: var(--font-chakra), sans-serif; - font-weight: 600; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + @apply bg-panel border border-border-color text-primary rounded-lg font-chakra font-semibold cursor-pointer transition-all duration-200 hover:bg-panel-hover hover:-translate-y-[2px] active:translate-y-[1px]; } .tactical-button:hover { @@ -208,15 +225,7 @@ p { } .tactical-button-primary { - background: rgba(0, 229, 255, 0.15); - border: 1px solid var(--neon-blue); - color: var(--neon-blue); - box-shadow: 0 0 10px var(--shadow-glow); - border-radius: 8px; - font-family: var(--font-chakra), sans-serif; - font-weight: 600; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); + @apply bg-neon-blue/15 border border-neon-blue text-neon-blue shadow-[0_0_10px_var(--shadow-glow)] rounded-lg font-chakra font-semibold cursor-pointer transition-all duration-200 hover:bg-neon-blue hover:text-base hover:shadow-[0_0_20px_var(--color-neon-blue)] active:translate-y-[1px]; } .tactical-button-primary:hover { @@ -225,148 +234,14 @@ p { box-shadow: 0 0 20px var(--neon-blue); } -/* --- UI LAYER --- */ -.ui-layer { - position: absolute; - inset: 0; - pointer-events: none; - display: flex; - justify-content: space-between; - padding: 8px; - z-index: 100; - overflow: hidden; -} - -/* --- ZONES --- */ -.zone-left, -.zone-right { - pointer-events: auto; -} - -.zone-left { - position: absolute; - top: 8px; - left: 8px; - z-index: 20; -} - -.zone-right { - position: absolute; - top: 8px; - right: 8px; - bottom: 24px; - display: flex; - flex-direction: column; - justify-content: space-between; - width: 44px; - z-index: 20; -} - -.zone-center { - position: absolute; - top: 8px; - left: 50%; - transform: translateX(-50%); - width: max-content; - max-width: calc(100vw - 120px); - display: flex; - flex-direction: column; - align-items: stretch; - gap: 8px; - pointer-events: none; - z-index: 10; -} - -/* --- CENTER CONTENTS --- */ -.search-block { - position: relative; - width: 100%; - margin: 0 auto; - pointer-events: auto; - transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); - display: flex; - align-items: center; -} - -.search-block:focus-within { - transform: scale(1.05); -} - -.search-input { - width: 100%; - height: 44px; - background: var(--bg-panel); - backdrop-filter: blur(12px); - border: 1px solid var(--border-color); - border-radius: 14px; - box-sizing: border-box; - padding: 0 40px 0 14px; - color: var(--text-primary); - font-size: 13px; - font-weight: 700; - font-family: var(--font-chakra); -} - -.search-input:focus { - outline: none; - border-color: rgba(255, 255, 255, 0.3); -} - -.search-icon-right { - position: absolute; - right: 12px; - top: 50%; - transform: translateY(-50%); - color: #aaa; - pointer-events: none; - z-index: 5; -} - -.filter-row { - display: flex; - justify-content: center; - gap: 8px; - width: 100%; - overflow-x: auto; - padding-top: 4px; - padding-bottom: 4px; - pointer-events: auto; -} - .no-scrollbar::-webkit-scrollbar { display: none; } -.filter-chip { - padding: 6px 14px; - border: 1px solid; - border-radius: 18px; - font-size: 10px; - font-weight: 900; - white-space: nowrap; - cursor: pointer; - font-family: var(--font-chakra); - transition: - transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), - background 0.2s; -} - /* --- SIDE CONTROLS --- */ .icon-button, .control-button { - width: 44px; - height: 44px; - background: var(--bg-panel); - backdrop-filter: blur(12px); - border: 1px solid var(--border-color); - border-radius: 12px; - color: var(--text-primary); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: transform 0.1s; - font-family: var(--font-chakra); + @apply w-11 h-11 bg-panel backdrop-blur-md border border-border-color rounded-xl text-primary flex items-center justify-center cursor-pointer transition-transform duration-100 font-chakra active:scale-95; } .icon-button:active, @@ -374,116 +249,6 @@ p { transform: scale(0.92); } -.tool-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.bottom-align { - margin-top: auto; -} - -.zoom-stack { - display: flex; - flex-direction: column; - background: var(--bg-panel); - backdrop-filter: blur(12px); - border: 1px solid var(--border-color); - border-radius: 12px; - overflow: hidden; -} - .control-button { - border: none; - border-radius: 0; -} - -.divider { - height: 1px; - background: var(--border-color); - width: 80%; - margin: 0 auto; -} - -.transit-system-container { - position: relative; - margin-top: 12px; - display: flex; - align-items: center; -} - -.transit-btn.active { - background: rgba(0, 229, 255, 0.15); - color: var(--neon-blue, #00e5ff); - border-color: var(--neon-blue, #00e5ff); -} - -.extruded-menu { - position: absolute; - left: 100%; - margin-left: 12px; - display: flex; - gap: 8px; - background: var(--bg-panel); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--border-color); - border-radius: 20px; - padding: 6px; - animation: extrudeRight 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; -} - -.route-node { - width: 32px; - height: 32px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-family: var(--font-chakra), sans-serif; - font-weight: 700; - font-size: 14px; - cursor: pointer; - border: 1px solid transparent; - transition: all 0.2s ease; -} - -.route-node:hover { - transform: scale(1.1); - background: var(--bg-panel-hover) !important; - color: var(--text-primary) !important; -} - -@keyframes extrudeRight { - from { - opacity: 0; - transform: translateX(-15px) scale(0.9); - } - - to { - opacity: 1; - transform: translateX(0) scale(1); - } -} - -/* --- HUD & ANIMATIONS --- */ - -@media (max-width: 768px) { - .profile-btn, - .theme-toggle { - display: none; - } - - .zone-left { - top: auto; - bottom: 24px; - display: flex; - flex-direction: column-reverse; - gap: 12px; - } - - .zone-left .transit-system-container { - margin-top: 0; - } -} + @apply border-none rounded-none; +} \ No newline at end of file diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 0f574e4..5171391 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -6,15 +6,15 @@ import { TRPCProvider } from "@/components/TRPCProvider"; import { ThemeProvider } from "@/lib/ThemeContext"; const chakra = Chakra_Petch({ - weight: ["300", "400", "500", "600", "700"], - subsets: ["latin"], - variable: "--font-chakra", + weight: ["300", "400", "500", "600", "700"], + subsets: ["latin"], + variable: "--font-chakra", }); const nunito = Nunito({ - weight: ["400", "700"], - subsets: ["latin"], - variable: "--font-nunito", + weight: ["400", "700"], + subsets: ["latin"], + variable: "--font-nunito", }); const cubaoFree = localFont({ @@ -36,24 +36,25 @@ const cubaoFreeWide = localFont({ }); export const metadata: Metadata = { - title: "UP WayPoint", - description: "Lead the way!", + title: "UP WayPoint", + description: "Lead the way!", }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - - - {children} - - - - - ); + return ( + + + + {children} + + + + ); } diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index f93d841..54cc4b7 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,11 +1,11 @@ "use client"; import { - APIProvider, - Map as GoogleMap, - AdvancedMarker, - ColorScheme, - MapCameraChangedEvent, + APIProvider, + Map as GoogleMap, + AdvancedMarker, + ColorScheme, + MapCameraChangedEvent, } from "@vis.gl/react-google-maps"; import { HeadsUpDisplay } from "@/components/HeadsUpDisplay"; import { NeonPin } from "@/components/NeonPin"; @@ -18,9 +18,9 @@ import { Polyline } from "@/components/Polyline"; import { Polygon } from "@/components/Polygon"; import { Sidebar } from "@/components/Sidebar"; import { - JEEPNEY_ROUTES, - CAMPUS_ZONES, - ZONE_CATEGORIES, + JEEPNEY_ROUTES, + CAMPUS_ZONES, + ZONE_CATEGORIES, } from "@/data/map-layers"; import { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { useTheme } from "@/lib/ThemeContext"; @@ -30,317 +30,298 @@ import { useSession } from "@/lib/auth-client"; import { skipToken } from "@tanstack/react-query"; export default function Home() { - const session = useSession(); - const params = useSearchParams(); - - const [hasPreselected, setHasPreselected] = useState(false); - - const { data: pins } = trpc.pin.getAll.useQuery(undefined, { - refetchOnWindowFocus: false, - }); - - const { data: currentUser } = trpc.user.getCurrent.useQuery( - session ? undefined : skipToken, - ); - - const { - mode, - selectedPinId, - selectPin, - clearSelection, - expandDetails, - toggleMenu, - toggleLock, - } = useWaypointState(); - const [activeFilter, setActiveFilter] = useState("all"); - const [searchQuery, setSearchQuery] = useState(""); - const [isAddingPin, setIsAddingPin] = useState(false); - - const [activeRoutes, setActiveRoutes] = useState([]); - - const handleToggleRoute = (routeId: string) => { - setActiveRoutes((prev) => - prev.includes(routeId) - ? prev.filter((id) => id !== routeId) - : [...prev, routeId], - ); - }; - - const [activeZoneCategories, setActiveZoneCategories] = useState( - [], - ); - - const handleToggleZoneCategory = (categoryId: string) => { - setActiveZoneCategories((prev) => - prev.includes(categoryId) - ? prev.filter((id) => id !== categoryId) - : [...prev, categoryId], - ); - }; - - const [cameraProps, setCameraProps] = useState({ - center: { lat: 14.6549, lng: 121.0645 }, - zoom: 19, - }); - - const handleCameraChange = useCallback((ev: MapCameraChangedEvent) => { - setCameraProps({ - center: ev.detail.center, - zoom: ev.detail.zoom, - }); - }, []); - - const mockUserLocation = { lat: 14.6549, lng: 121.0645 }; - const mockHeading = 45; - - const { theme } = useTheme(); - - const [pendingPinCoords, setPendingPinCoords] = useState<{ - lat: number; - lng: number; - } | null>(null); - const cursorRef = useRef(null); - - const pinsParsed = useMemo(() => { - return ( - pins - ?.filter((p) => p !== null) - .filter((p) => p.id && p.latitude && p.longitude) || [] - ); - }, [pins]); - - const activePinObj = useMemo(() => { - return pinsParsed.find((p) => p.id === selectedPinId); - }, [pinsParsed, selectedPinId]); - - useEffect(() => { - if (!isAddingPin) return; - const handleMouseMove = (e: MouseEvent) => { - if (cursorRef.current) { - cursorRef.current.style.transform = `translate(${e.clientX - 24}px, ${e.clientY - 24}px)`; - } - }; - - window.addEventListener("mousemove", handleMouseMove); - - return () => window.removeEventListener("mousemove", handleMouseMove); - }, [isAddingPin]); - - useEffect(() => { - if (params.has("pin") && !hasPreselected) { - const preselectedPin = params.get("pin") as string; - selectPin(preselectedPin); - setHasPreselected(true); - } - }, [params, selectPin, hasPreselected]); - - return ( - -
- {/* MAP LAYER */} - { - if (isAddingPin) { - const lat = e.detail.latLng?.lat; - const lng = e.detail.latLng?.lng; - - if (lat && lng) { - setPendingPinCoords({ lat, lng }); - setIsAddingPin(false); - } - } else { - clearSelection(); - } - }} - restriction={{ - latLngBounds: { - north: 14.663668, - south: 14.645343, - east: 121.075583, - west: 121.05536, - }, - strictBounds: false, - }} - > - {pinsParsed.map((pinData) => { - const matchesCategory = - activeFilter === "all" || - pinData.pinTags.map((t) => t.tag.title).includes(activeFilter); - const matchesSearch = - pinData.title - ?.toLowerCase() - .includes(searchQuery.toLowerCase()) || - pinData.description - ?.toLowerCase() - .includes(searchQuery.toLowerCase()); - - const isVisible = - !!(matchesCategory && matchesSearch) && - (currentUser?.userRole === "admin" - ? pinData.status !== "DELETED" - : currentUser?.userRole === "user" && - pinData.ownerId === currentUser?.id - ? pinData.status !== "DELETED" - : pinData.status === "ACTIVE"); - - return ( - - selectPin(pinData.id || "")} - /> - - ); - })} - - - - - - {activePinObj && ( - - )} - - {CAMPUS_ZONES.map((zone) => { - if (!activeZoneCategories.includes(zone.categoryId)) return null; - - const categoryDef = ZONE_CATEGORIES.find( - (c) => c.id === zone.categoryId, - ); - const zoneColor = categoryDef ? categoryDef.color : "var(--bg-panel)"; - - return ( - - ); - })} - - {JEEPNEY_ROUTES.map((route) => { - if (!activeRoutes.includes(route.id)) return null; - - return ( - - ); - })} - - - {/* TOP BAR */} - - - {/* TARGETING CROSSHAIR (Only visible when armed) */} - {isAddingPin && ( -
- {/* Tactical Crosshair SVG */} - - - - - - - - -
- )} - - {/* UI LAYER */} - { - clearSelection(); - setIsAddingPin(true); - }} - /> - - - - {pendingPinCoords && ( - setPendingPinCoords(null)} - onSave={(newPinId) => { - // HANDLE NEW PIN WITH INVALIDATE - // setPins((prev) => [...prev, newPin]); - selectPin(newPinId); - setPendingPinCoords(null); - }} - /> - )} -
-
- ); + const session = useSession(); + const params = useSearchParams(); + + const [hasPreselected, setHasPreselected] = useState(false); + + const { data: pins } = trpc.pin.getAll.useQuery(undefined, { + refetchOnWindowFocus: false, + }); + + const { data: currentUser } = trpc.user.getCurrent.useQuery( + session ? undefined : skipToken, + ); + + const { + mode, + selectedPinId, + selectPin, + clearSelection, + toggleMenu, + toggleLock, + } = useWaypointState(); + const [activeFilter, setActiveFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + const [isAddingPin, setIsAddingPin] = useState(false); + + const [activeRoutes, setActiveRoutes] = useState([]); + + const handleToggleRoute = (routeId: string) => { + setActiveRoutes((prev) => + prev.includes(routeId) + ? prev.filter((id) => id !== routeId) + : [...prev, routeId], + ); + }; + + const [activeZoneCategories, setActiveZoneCategories] = useState( + [], + ); + + const handleToggleZoneCategory = (categoryId: string) => { + setActiveZoneCategories((prev) => + prev.includes(categoryId) + ? prev.filter((id) => id !== categoryId) + : [...prev, categoryId], + ); + }; + + const [cameraProps, setCameraProps] = useState({ + center: { lat: 14.6549, lng: 121.0645 }, + zoom: 19, + }); + + const handleCameraChange = useCallback((ev: MapCameraChangedEvent) => { + setCameraProps({ + center: ev.detail.center, + zoom: ev.detail.zoom, + }); + }, []); + + const mockUserLocation = { lat: 14.6549, lng: 121.0645 }; + const mockHeading = 45; + + const { theme } = useTheme(); + + const [pendingPinCoords, setPendingPinCoords] = useState<{ + lat: number; + lng: number; + } | null>(null); + const cursorRef = useRef(null); + + const pinsParsed = useMemo(() => { + return ( + pins + ?.filter((p) => p !== null) + .filter((p) => p.id && p.latitude && p.longitude) || [] + ); + }, [pins]); + + const activePinObj = useMemo(() => { + return pinsParsed.find((p) => p.id === selectedPinId); + }, [pinsParsed, selectedPinId]); + + useEffect(() => { + if (!isAddingPin) return; + const handleMouseMove = (e: MouseEvent) => { + if (cursorRef.current) { + cursorRef.current.style.transform = `translate(${e.clientX - 24}px, ${e.clientY - 24}px)`; + } + }; + + window.addEventListener("mousemove", handleMouseMove); + + return () => window.removeEventListener("mousemove", handleMouseMove); + }, [isAddingPin]); + + useEffect(() => { + if (params.has("pin") && !hasPreselected) { + const preselectedPin = params.get("pin") as string; + selectPin(preselectedPin); + setHasPreselected(true); + } + }, [params, selectPin, hasPreselected]); + + return ( + +
+ {/* MAP LAYER */} + { + if (isAddingPin) { + const lat = e.detail.latLng?.lat; + const lng = e.detail.latLng?.lng; + + if (lat && lng) { + setPendingPinCoords({ lat, lng }); + setIsAddingPin(false); + } + } else { + clearSelection(); + } + }} + restriction={{ + latLngBounds: { + north: 14.663668, + south: 14.645343, + east: 121.075583, + west: 121.05536, + }, + strictBounds: false, + }} + > + {pinsParsed.map((pinData) => { + const matchesCategory = + activeFilter === "all" || + pinData.pinTags.map((t) => t.tag.title).includes(activeFilter); + const matchesSearch = + pinData.title + ?.toLowerCase() + .includes(searchQuery.toLowerCase()) || + pinData.description + ?.toLowerCase() + .includes(searchQuery.toLowerCase()); + + const isVisible = + !!(matchesCategory && matchesSearch) && + (currentUser?.userRole === "admin" + ? pinData.status !== "DELETED" + : currentUser?.userRole === "user" && + pinData.ownerId === currentUser?.id + ? pinData.status !== "DELETED" + : pinData.status === "ACTIVE"); + + return ( + + selectPin(pinData.id || "")} + /> + + ); + })} + + + + + + {activePinObj && ( + + )} + + {CAMPUS_ZONES.map((zone) => { + if (!activeZoneCategories.includes(zone.categoryId)) return null; + + const categoryDef = ZONE_CATEGORIES.find( + (c) => c.id === zone.categoryId, + ); + const zoneColor = categoryDef + ? categoryDef.color + : "var(--bg-panel)"; + + return ( + + ); + })} + + {JEEPNEY_ROUTES.map((route) => { + if (!activeRoutes.includes(route.id)) return null; + + return ( + + ); + })} + + + {/* TOP BAR */} + + + {/* TARGETING CROSSHAIR (Only visible when armed) */} + {isAddingPin && ( +
+ {/* Tactical Crosshair SVG */} + + + + + + + + +
+ )} + + {/* UI LAYER */} + { + clearSelection(); + setIsAddingPin(true); + }} + /> + + + + {pendingPinCoords && ( + setPendingPinCoords(null)} + onSave={(newPinId) => { + // HANDLE NEW PIN WITH INVALIDATE + // setPins((prev) => [...prev, newPin]); + selectPin(newPinId); + setPendingPinCoords(null); + }} + /> + )} +
+
+ ); } diff --git a/apps/web/app/sign-in/page.tsx b/apps/web/app/sign-in/page.tsx index 0441d2a..90a984d 100644 --- a/apps/web/app/sign-in/page.tsx +++ b/apps/web/app/sign-in/page.tsx @@ -4,263 +4,108 @@ import { authClient } from "@/lib/auth-client"; import { useRouter } from "next/navigation"; export default function SignIn() { - const handleLogin = async () => { - await authClient.signIn.social({ - provider: "google", - callbackURL: "/dashboard", - }); - }; - - const router = useRouter(); - - const goToMap = () => router.push("/"); - - return ( -
- {/* BACKGROUND GLOW */} -
- - {/* LOGIN CARD */} -
- {/* HEADER SECTION */} -
-
- - - -
- -

UP WayPoint

-
-
- {/* Primary Action: Go to Map */} - - - {/* AUTH BUTTON */} - -
- {/* FOOTER */} - -
- - -
- ); + const handleLogin = async () => { + await authClient.signIn.social({ + provider: "google", + callbackURL: "/dashboard", + }); + }; + + const router = useRouter(); + + const goToMap = () => router.push("/"); + + return ( +
+ {/* BACKGROUND GLOW */} +
+ + {/* LOGIN CARD */} +
+ {/* HEADER SECTION */} +
+
+ + + +
+ +

+ UP WayPoint +

+
+ +
+ {/* Primary Action: Go to Map */} + + + {/* AUTH BUTTON */} + +
+ + {/* FOOTER */} + +
+
+ ); } diff --git a/apps/web/components/AddPinModal.tsx b/apps/web/components/AddPinModal.tsx index d6feee1..6f9e62d 100644 --- a/apps/web/components/AddPinModal.tsx +++ b/apps/web/components/AddPinModal.tsx @@ -12,298 +12,194 @@ import { getPinColor } from "@/data/pin-categories"; type Pin = Omit; interface AddPinModalProps { - coords: { lat: number; lng: number }; - onSave: (pinId: string) => void; - onCancel: () => void; + coords: { lat: number; lng: number }; + onSave: (pinId: string) => void; + onCancel: () => void; } async function uploadImages(files: File[]): Promise { - const formData = new FormData(); - files.forEach((file) => { - formData.append("images", file); - }); - - const res = await fetch("/api/upload", { method: "POST", body: formData }); - const { urls } = await res.json(); - return urls; + const formData = new FormData(); + files.forEach((file) => { + formData.append("images", file); + }); + + const res = await fetch("/api/upload", { method: "POST", body: formData }); + const { urls } = await res.json(); + return urls; } export function AddPinModal({ coords, onSave, onCancel }: AddPinModalProps) { - const pinCreationSchema = z.object({ - title: z.string().min(1), - description: z.string().optional(), - latitude: z.number().min(-90).max(90), - longitude: z.number().min(-180).max(180), - tags: z.array(z.string()), - images: z - .instanceof(FileList) - .transform((list) => Array.from(list)) - .pipe(z.array(fileSchema).max(10)), - }); - - type pinCreationSchemaType = z.infer; - - const utils = trpc.useUtils(); - const createPin = trpc.pin.userCreate.useMutation({ - onSuccess: (newPin) => { - utils.pin.getAll.invalidate(); - if (!newPin) return; - onSave(newPin.id); - }, - }); - const { data: tagsData } = trpc.tag.getAll.useQuery(); - - const formMethods = useForm({ - defaultValues: { tags: [] }, - resolver: zodResolver(pinCreationSchema), - }); - - const onSubmit = async (data: pinCreationSchemaType) => { - let urls: string[] = []; - if (data.images.length > 0) urls = await uploadImages(data.images); - const newPin: Pin = { - title: data.title.trim(), - description: data.description?.trim(), - latitude: coords.lat, - longitude: coords.lng, - tags: data.tags, - imageURLs: urls, - }; - - createPin.mutate(newPin); - }; - - const handleCancel = () => { - formMethods.clearErrors(); - formMethods.reset(); - onCancel(); - }; - - const tags = formMethods.watch("tags"); - - useEffect(() => { - formMethods.setValue("latitude", coords.lat); - formMethods.setValue("longitude", coords.lng); - }, [formMethods, coords]); - - return ( -
-
-
- - - - -

ADD NEW PIN

-
- -
-
- PIN TITLE - -
- -
- DESCRIPTION -