diff --git a/apps/web/app/admin/page.tsx b/apps/web/app/admin/page.tsx
index 23b827b..d306298 100644
--- a/apps/web/app/admin/page.tsx
+++ b/apps/web/app/admin/page.tsx
@@ -1,555 +1,869 @@
"use client";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { signOut } from "@/lib/auth-client";
import { trpc } from "@/lib/trpc";
import { PIN_CATEGORIES, getPinColor } from "@/data/pin-categories";
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 [isSidebarOpen, setIsSidebarOpen] = useState(false);
- const { theme, toggleTheme } = useTheme();
- const [activeSection, setActiveSection] = useState("overview");
-
- 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 globalPinStats = {
- totalPins: 1240,
- verifiedPins: 1105,
- pendingPins: 85,
- rejectedPins: 50,
- categoryBreakdown: {
- academic: 450,
- food: 320,
- social: 150,
- transit: 200,
- utility: 120,
- }
- };
-
- const globalVerificationRate = Math.round((globalPinStats.verifiedPins / globalPinStats.totalPins) * 100) || 0;
-
- const globalUserStats = {
- totalUsers: 342,
- totalComments: 1840,
- avgPins: 3.6,
- avgComments: 5.3,
- newUsers7Days: 14,
- newUsers30Days: 45,
- };
-
- const globalPendingPins = [
- { id: "gp1", title: "Palma Hall Annex", lat: 14.6534, lng: 121.0691, type: "academic", submittedBy: "u1" },
- { id: "gp2", title: "KNL Tricycle Terminal", lat: 14.6552, lng: 121.0621, type: "transit", submittedBy: "u2" },
- { id: "gp3", title: "Gyud Food", lat: 14.6542, lng: 121.0665, type: "food", submittedBy: "u3" },
- ];
-
- const globalVerifiedPins = [
- { id: "v1", title: "Main Library", lat: 14.6540, lng: 121.0660, type: "academic", submittedBy: "u1" },
- { id: "v2", title: "Area 2 Kiosk 4", lat: 14.6530, lng: 121.0685, type: "food", submittedBy: "u2" },
- { id: "v3", title: "AS Parking", lat: 14.6538, lng: 121.0688, type: "utility", submittedBy: "u3" },
- ];
-
- const recentUsers = [
- { id: "u1", name: "User 1", email: "user1@up.edu.ph", joinedAt: "2 hours ago" },
- { id: "u2", name: "User 2", email: "user2@up.edu.ph", joinedAt: "5 hours ago" },
- { id: "u3", name: "User 3", email: "user3@up.edu.ph", joinedAt: "1 day ago" },
- { id: "u4", name: "User 4", email: "user4@up.edu.ph", joinedAt: "2 days ago" },
- ];
-
- const topUsers = [
- { id: "u1", name: "User 1", pinCount: 142, rank: 1 },
- { id: "u2", name: "User 2", pinCount: 89, rank: 2 },
- { id: "u3", name: "User 3", pinCount: 75, rank: 3 },
- { id: "u4", name: "User 4", pinCount: 60, rank: 4 },
- ];
-
- return (
-
- {/* --- MOBILE OVERLAY --- */}
- {isSidebarOpen && (
-
setIsSidebarOpen(false)}
- />
- )}
-
- {/* --- SIDEBAR --- */}
-
-
-
- {/* --- HEADER --- */}
-
-
- {/* --- 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 IN MAP
- {globalPinStats.totalPins}
-
-
- AWAITING ACTION
-
- {globalPinStats.pendingPins}
-
-
-
-
-
-
- GLOBAL VERIFICATION
-
- {globalVerificationRate}%
-
-
-
-
- {globalPinStats.verifiedPins} VERIFIED
- {globalPinStats.pendingPins} PENDING
- {globalPinStats.rejectedPins} REJECTED
-
-
-
-
-
CATEGORY BREAKDOWN
-
- {PIN_CATEGORIES.map((category) => {
- const count = globalPinStats.categoryBreakdown[category.id as keyof typeof globalPinStats.categoryBreakdown] || 0;
- const percentage = globalPinStats.totalPins > 0 ? (count / globalPinStats.totalPins) * 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
-
-
-
-
- {globalPendingPins.map((pin) => {
- const color = getPinColor(pin.type);
- return (
-
-
-
- {pin.title.charAt(0).toUpperCase()}
-
-
-
- {pin.title}
-
- By {pin.submittedBy} • {pin.lat.toFixed(4)}, {pin.lng.toFixed(4)}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
RECENTLY VERIFIED PINS
-
-
-
-
- {globalVerifiedPins.map((pin) => {
- const color = getPinColor(pin.type);
- return (
-
-
-
- {pin.title.charAt(0).toUpperCase()}
-
-
-
- {pin.title}
-
- By {pin.submittedBy} • {pin.lat.toFixed(4)}, {pin.lng.toFixed(4)}
-
-
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
- USER MANAGEMENT
-
-
-
-
NEWEST USERS
-
-
-
- {recentUsers.map((user) => (
-
-
-
- {user.name.charAt(0).toUpperCase()}
-
-
- {user.name}
-
- {user.email} • {user.joinedAt}
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
TOP USERS BY PINS
-
-
-
- {topUsers.map((user, index) => (
-
-
-
-
- {user.name.charAt(0).toUpperCase()}
-
-
- {user.name}
- Rank #{user.rank} Operator
-
-
-
-
-
- {user.pinCount}
-
- PINS
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
+
+ );
+}
diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx
index edf1f13..50afe41 100644
--- a/apps/web/app/dashboard/page.tsx
+++ b/apps/web/app/dashboard/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { signOut } from "@/lib/auth-client";
import { trpc } from "@/lib/trpc";
@@ -8,413 +8,591 @@ import { PIN_CATEGORIES, getPinColor } from "@/data/pin-categories";
import { useTheme } from "@/lib/ThemeContext";
export default function Dashboard() {
- const router = useRouter();
- const { data, isLoading } = trpc.user.getCurrent.useQuery();
- const [isSidebarOpen, setIsSidebarOpen] = useState(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 mockStats = {
- totalPins: 42,
- verifiedPins: 38,
- pendingPins: 3,
- rejectedPins: 1,
- comments: 128,
- categoryBreakdown: {
- academic: 12,
- food: 15,
- social: 5,
- transit: 8,
- utility: 2,
- },
- pendingList: [
- { id: "p1", title: "Quezon Hall", lat: 14.6549, lng: 121.0645, type: "academic" },
- { id: "p2", title: "Area 2", lat: 14.6532, lng: 121.0681, type: "food" },
- { id: "p3", title: "Sunken Garden", lat: 14.6544, lng: 121.0673, type: "social" },
- ],
- recentList: [
- { id: "r1", title: "CS Library", lat: 14.6538, lng: 121.0694, type: "academic" },
- { id: "r2", title: "TOKI Jeepney Stop", lat: 14.6551, lng: 121.0621, type: "transit" },
- ]
- };
-
- const verificationRate = Math.round((mockStats.verifiedPins / mockStats.totalPins) * 100) || 0;
-
- return (
-
- {/* --- MOBILE OVERLAY --- */}
- {isSidebarOpen && (
-
setIsSidebarOpen(false)}
- />
- )}
-
- {/* --- SIDEBAR --- */}
-
-
-
- {/* --- HEADER --- */}
-
-
- {/* --- MAIN --- */}
-
-
-
-
- {isLoading ? "LOADING..." : `Welcome, ${data?.name ? data.name.toUpperCase() : "UNKNOWN"}!`}
-
-
You made it!
-
-
- {/* --- DASHBOARD GRID --- */}
-
-
-
-
-
YOUR PROFILE
-
- {(data as any)?.role === "ADMIN" ? "ADMIN" : "REGULAR USER"}
-
-
-
-
-
-
-
- {data?.name ? data.name.charAt(0).toUpperCase() : "O"}
-
-
-
- {data?.name || "UNKNOWN NAME"}
- {(data as any)?.email || "UNKNOWN EMAIL"}
-
-
-
-
-
-
BIO
- {!isEditingBio && (
-
- )}
-
-
- {isEditingBio ? (
-
- ) : (
-
- {(data as any)?.bio || "No bio here. Click EDIT to add a short bio about yourself!"}
-
- )}
-
-
-
-
-
-
-
YOUR STATISTICS
-
- SYNCED
-
-
-
-
- {/* Top Stats Grid */}
-
-
- TOTAL PINS ADDED
- {mockStats.totalPins}
-
-
- TOTAL COMMENTS
- {mockStats.comments}
-
-
-
- {/* Verification Integrity Bar */}
-
-
- VERIFICATIONS
- {verificationRate}%
-
-
-
- {mockStats.verifiedPins} VERIFIED
- {mockStats.pendingPins} PENDING
- {mockStats.rejectedPins} REJECTED
-
-
-
- {/* Category Distribution */}
-
-
CATEGORY DISTRIBUTION
-
- {PIN_CATEGORIES.map((category) => {
- const count = mockStats.categoryBreakdown[category.id as keyof typeof mockStats.categoryBreakdown] || 0;
- const percentage = mockStats.totalPins > 0 ? (count / mockStats.totalPins) * 100 : 0;
-
- return (
-
-
- {category.label}
- {count}
-
-
-
- );
- })}
-
-
-
-
-
-
-
-
YOUR PENDING PINS
- {mockStats.pendingList.length}
-
-
-
- {mockStats.pendingList.map((pin) => {
- const color = getPinColor(pin.type);
- return (
-
-
-
- {pin.title.charAt(0).toUpperCase()}
-
-
-
- {pin.title}
-
- {pin.lat.toFixed(4)}, {pin.lng.toFixed(4)}
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
YOUR RECENT PINS
-
- {mockStats.recentList.length}
-
-
-
-
- {mockStats.recentList.map((pin) => {
- const color = getPinColor(pin.type);
- return (
-
-
-
- {pin.title.charAt(0).toUpperCase()}
-
-
-
- {pin.title}
-
- {pin.lat.toFixed(4)}, {pin.lng.toFixed(4)}
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
+
+ );
+}
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 4491f46..2b970b7 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -17,16 +17,30 @@ import { TargetLine } from "@/components/TargetLine";
import { Polyline } from "@/components/Polyline";
import { Polygon } from "@/components/Polygon";
import { Sidebar } from "@/components/Sidebar";
-import { JEEPNEY_ROUTES, CAMPUS_ZONES, ZONE_CATEGORIES } from "@/data/map-layers";
+import {
+ JEEPNEY_ROUTES,
+ CAMPUS_ZONES,
+ ZONE_CATEGORIES,
+} from "@/data/map-layers";
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
import { useTheme } from "@/lib/ThemeContext";
import { trpc } from "@/lib/trpc";
+import { useSearchParams } from "next/navigation";
+import { useSession } from "@/lib/auth-client";
+import { skipToken } from "@tanstack/react-query";
export default function Home() {
+ const session = useSession();
+ const params = useSearchParams();
+
const { data: pins } = trpc.pin.getAll.useQuery(undefined, {
refetchOnWindowFocus: false,
});
+ const { data: currentUser } = trpc.user.getCurrent.useQuery(
+ session ? undefined : skipToken,
+ );
+
const {
mode,
selectedPinId,
@@ -42,33 +56,37 @@ export default function Home() {
const [activeRoutes, setActiveRoutes] = useState([]);
- const handleToggleRoute = (routeId: string) => {
- setActiveRoutes((prev) =>
- prev.includes(routeId)
- ? prev.filter(id => id !== routeId)
- : [...prev, routeId]
- );
- };
+ const handleToggleRoute = (routeId: string) => {
+ setActiveRoutes((prev) =>
+ prev.includes(routeId)
+ ? prev.filter((id) => id !== routeId)
+ : [...prev, routeId],
+ );
+ };
- const [activeZoneCategories, setActiveZoneCategories] = useState([]);
+ const [activeZoneCategories, setActiveZoneCategories] = useState(
+ [],
+ );
- const handleToggleZoneCategory = (categoryId: string) => {
- setActiveZoneCategories((prev) =>
- prev.includes(categoryId) ? prev.filter(id => id !== categoryId) : [...prev, categoryId]
- );
- };
+ 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,
- });
+ center: { lat: 14.6549, lng: 121.0645 },
+ zoom: 19,
+ });
- const handleCameraChange = useCallback((ev: MapCameraChangedEvent) => {
- setCameraProps({
- center: ev.detail.center,
- zoom: ev.detail.zoom,
- });
- }, []);
+ 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;
@@ -90,8 +108,8 @@ export default function Home() {
}, [pins]);
const activePinObj = useMemo(() => {
- return pinsParsed.find((p) => p.id === selectedPinId);
- }, [pinsParsed, selectedPinId]);
+ return pinsParsed.find((p) => p.id === selectedPinId);
+ }, [pinsParsed, selectedPinId]);
useEffect(() => {
if (!isAddingPin) return;
@@ -106,6 +124,13 @@ export default function Home() {
return () => window.removeEventListener("mousemove", handleMouseMove);
}, [isAddingPin]);
+ useEffect(() => {
+ if (params.has("pin")) {
+ const preselectedPin = params.get("pin") as string;
+ selectPin(preselectedPin);
+ }
+ }, [params, selectPin]);
+
return (
-
-
-
- {activePinObj && (
-
- )}
+
+
+
+
+ {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 : "#FFFFFF";
-
- return (
-
- );
- })}
+ if (!activeZoneCategories.includes(zone.categoryId)) return null;
+
+ const categoryDef = ZONE_CATEGORIES.find(
+ (c) => c.id === zone.categoryId,
+ );
+ const zoneColor = categoryDef ? categoryDef.color : "#FFFFFF";
+
+ return (
+
+ );
+ })}
{JEEPNEY_ROUTES.map((route) => {
- if (!activeRoutes.includes(route.id)) return null;
-
- return (
-
- );
- })}
+ if (!activeRoutes.includes(route.id)) return null;
+
+ return (
+
+ );
+ })}
{/* TOP BAR */}
+ />
{/* TARGETING CROSSHAIR (Only visible when armed) */}
{isAddingPin && (
@@ -293,10 +320,7 @@ export default function Home() {
}}
/>
-
+
{pendingPinCoords && (
-
PIN TYPES (select all that apply)
-
- {tagsData?.map((t) => {
- const isActive = tags.includes(t.id);
- const tagColor = getPinColor(t.title);
-
- return (
-
- );
- })}
-
-
+ PIN TYPE
+
+ {tagsData?.map((t) => {
+ const isActive = tags.includes(t.id);
+ const tagColor = getPinColor(t.title);
+
+ return (
+
+ );
+ })}
+
+
-
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/components/EditPinModal.tsx b/apps/web/components/EditPinModal.tsx
new file mode 100644
index 0000000..41773a4
--- /dev/null
+++ b/apps/web/components/EditPinModal.tsx
@@ -0,0 +1,321 @@
+"use client";
+
+import { trpc } from "@/lib/trpc";
+import type { PinRouterInputs, PinRouterOutputs } from "@repo/api";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useEffect } from "react";
+import z from "zod";
+import { fileSchema } from "@repo/api/schemas";
+import { getPinColor } from "@/data/pin-categories";
+type Pin = {
+ id?: string | undefined;
+ createdAt?: string | undefined;
+ updatedAt?: string | undefined;
+ title?: string | undefined;
+ pinTags?:
+ | {
+ createdAt: string;
+ updatedAt: string;
+ tag: {
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ title: string;
+ color: string | null;
+ };
+ pinId: string;
+ tagId: string;
+ }[]
+ | undefined;
+ status?: string | undefined;
+ latitude?: number | undefined;
+ longitude?: number | undefined;
+ description?: string | null;
+ ownerId?: string | undefined;
+};
+
+type UpdatePin = PinRouterInputs["update"];
+
+interface IEditPinModalProps {
+ onSave: (pinId: string) => void;
+ onCancel: () => void;
+ pin: Pin;
+}
+
+export function EditPinModal({ onSave, onCancel, pin }: IEditPinModalProps) {
+ const pinCreationSchema = z.object({
+ title: z.string().min(1).optional(),
+ description: z.string().optional(),
+ tags: z.array(z.string()).optional(),
+ });
+
+ type pinCreationSchemaType = z.infer;
+
+ const updatePin = trpc.pin.update.useMutation({
+ onSuccess: (newPin) => {
+ if (!newPin) return;
+ onSave(newPin.id);
+ },
+ });
+ const { data: tagsData } = trpc.tag.getAll.useQuery();
+
+ const formMethods = useForm({
+ defaultValues: {
+ tags: pin.pinTags?.map((pt) => pt.tagId),
+ title: pin.title,
+ description: pin.description || "",
+ },
+ resolver: zodResolver(pinCreationSchema),
+ });
+
+ const onSubmit = async (data: pinCreationSchemaType) => {
+ if (!pin.id) {
+ handleCancel();
+ return;
+ }
+
+ const dirtyFields = Object.keys(formMethods.formState.dirtyFields);
+ console.log(dirtyFields);
+
+ updatePin.mutate({
+ id: pin.id,
+ title: dirtyFields.includes("title") ? data.title : undefined,
+ description: dirtyFields.includes("description")
+ ? data.description
+ : undefined,
+ tags: dirtyFields.includes("tags") ? data.tags || [] : [],
+ });
+ };
+
+ const handleCancel = () => {
+ formMethods.clearErrors();
+ formMethods.reset();
+ onCancel();
+ };
+
+ const tags = formMethods.watch("tags");
+
+ return (
+
+
+
+
+
EDIT PIN
+ {formMethods.formState.errors.tags?.message}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/components/ExpandedPinView.tsx b/apps/web/components/ExpandedPinView.tsx
index 71d233a..efd43e1 100644
--- a/apps/web/components/ExpandedPinView.tsx
+++ b/apps/web/components/ExpandedPinView.tsx
@@ -8,105 +8,115 @@ import Image from "next/image";
import { useState } from "react";
import { useForm } from "react-hook-form";
import z from "zod";
+import { EditPinModal } from "./EditPinModal";
interface ExpandedPinViewProps {
- pinId: string;
- onClose: () => void;
+ pinId: string;
+ onClose: () => void;
}
type Comment = {
- id: string;
- createdAt: string;
- updatedAt: string;
- ownerId: string;
- pinId: string;
- message: string;
- parentId: string | null;
- deletedAt: string | null;
- replies: Comment[];
- authorName: string;
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ ownerId: string;
+ pinId: string;
+ message: string;
+ parentId: string | null;
+ deletedAt: string | null;
+ replies: Comment[];
+ authorName: string;
};
const commentSchema = z.object({
- message: z.string(),
+ message: z.string(),
});
type commentSchemaType = z.infer;
const CommentNode = ({
- comment,
- depth = 0,
+ comment,
+ depth = 0,
}: {
- comment: Comment;
- depth: number;
+ comment: Comment;
+ depth: number;
}) => {
- const { data: sessionData } = useSession();
- const utils = trpc.useUtils();
- const createComment = trpc.comment.create.useMutation({
- onSuccess(output) {
- utils.pin.getById.invalidate();
- setIsReplying(false);
- },
- });
- const formMethods = useForm({ resolver: zodResolver(commentSchema) });
- const [isReplying, setIsReplying] = useState(false);
- if (depth > 3) return null;
-
- function onSubmit(data: commentSchemaType) {
- createComment.mutate({
- message: data.message,
- pinId: comment.pinId,
- parentId: comment.id,
- });
- }
-
- return (
- 0 ? "is-reply" : ""}`}>
-
- {comment.authorName}
-
- {new Date(comment.createdAt).toLocaleString("default")}
-
-
-
-
{comment.message}
-
-
- {!isReplying ? (
- sessionData &&
- depth < 3 && (
- setIsReplying(true)}
- >
- REPLY
-
- )
- ) : (
-
- )}
-
-
- {comment.replies && comment.replies.length > 0 && (
-
- {comment.replies.map((reply) => (
-
- ))}
-
- )}
-
-
-
- );
+
+ );
};
export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) {
- const utils = trpc.useUtils();
- const { data: sessionData } = useSession();
- const { data: pin } = trpc.pin.getById.useQuery(
- { id: pinId },
- { refetchOnWindowFocus: false },
- );
-
- const createComment = trpc.comment.create.useMutation({
- onSuccess(output) {
- utils.pin.getById.invalidate();
- setIsReplying(false);
- },
- });
- const deletePin = trpc.pin.userDelete.useMutation({
- onSuccess(output) {
- utils.pin.getAll.invalidate();
- onClose();
- },
- });
-
- const formMethods = useForm({ resolver: zodResolver(commentSchema) });
- const [isReplying, setIsReplying] = useState(false);
-
- function onSubmit(data: commentSchemaType) {
- createComment.mutate({
- message: data.message,
- pinId: pinId,
- });
- }
-
- const [isDeleting, setIsDeleting] = useState(false);
-
- function onDelete() {
- deletePin.mutate({ id: pinId });
- }
-
- const color = getPinColor(
- pin?.pinTags ? pin.pinTags[0]?.tag.title || "" : "",
- );
-
- const getStatusDisplay = (status: string | undefined) => {
- switch (status) {
- case "VERIFIED":
- return {
- text: "VERIFIED",
- // color: "var(--neon-green, #00FF99)",
- icon: (
-
- )
- };
- case "PENDING_VERIFICATION":
- return {
- text: "PENDING",
- // color: "var(--neon-yellow, #FFD700)",
- icon: (
-
- )
- };
- case "REJECTED":
- return {
- text: "REJECTED",
- // color: "#ff4d4d",
- icon: (
-
- )
- };
- default:
- return {
- text: status || "UNKNOWN",
- color: "var(--text-secondary)",
- icon: (
-
- )
- };
- }
- };
-
- const statusData = getStatusDisplay(pin?.status);
-
- return (
-
-
e.stopPropagation()}>
-
- {/* HEADER */}
-
-
-
- {pin?.pinTags?.map((pt) => pt.tag.title).join(", ")}
-
-
{pin?.title}
-
-
-
- {sessionData?.user?.id === pin?.ownerId && (
-
{
- // TODO: Wire up to Edit Form Modal
- console.log("Edit Pin:", pinId);
- }}
- title="Edit Waypoint"
- >
-
-
- )}
-
-
-
-
-
-
-
- {/* HORIZONTAL BENTO GALLERY OR FALLBACK */}
- {pin?.images && pin.images.length > 0 ? (
-
- {pin.images.map((img) => (
-
-
-
- ))}
-
- ) : (
-
-
-
NO IMAGES AVAILABLE
-
- )}
-
- {/* BODY: INTEL DASHBOARD */}
-
-
{pin?.description}
-
-
- {/* PIN ID */}
-
- PIN ID
-
- {pin?.id?.padStart(7, "0")}
-
-
-
- {/* OWNER */}
-
- OWNED BY
- {pin?.ownerId}
-
-
- {/* COORDINATES */}
-
- COORDINATES (LAT, LNG)
-
- {pin?.latitude?.toFixed(6)}, {pin?.longitude?.toFixed(6)}
-
-
-
- {/* STATUS */}
-
- STATUS
-
- {statusData.icon}
- {statusData.text}
-
-
-
- {/* TIMESTAMPS */}
-
- CREATED AT
-
- {new Date(pin?.createdAt || "").toLocaleString("default", {
- month: "long",
- year: "numeric",
- day: "2-digit",
- })}
-
-
-
- LAST UPDATED
-
- {new Date(pin?.updatedAt || "").toLocaleString("default", {
- month: "long",
- year: "numeric",
- day: "2-digit",
- })}
-
-
-
-
- {/* OWNER ACTIONS */}
- {!isDeleting && sessionData?.user.id === pin?.ownerId && (
-
setIsDeleting(true)}>
- DELETE PIN
-
- )}
-
- {isDeleting && (
-
-
Are you sure you want to permanently delete this pin?
-
-
- CONFIRM DELETE
-
- setIsDeleting(false)}>
- CANCEL
-
-
-
- )}
-
-
-
FORUM
- {!isReplying ? (
- sessionData && (
-
setIsReplying(true)}
- >
- + ADD COMMENT
-
- )
- ) : (
-
- )}
-
- {pin?.comments?.map((thread) => (
-
- ))}
-
-
-
-
-
-
-
-
-
+
+ );
+
+ return (
+ // biome-ignore lint/a11y/noStaticElementInteractions:
+ // biome-ignore lint/a11y/useKeyWithClickEvents:
+
+ {/** biome-ignore lint/a11y/noStaticElementInteractions:
*/}
+ {/** biome-ignore lint/a11y/useKeyWithClickEvents: */}
+ e.stopPropagation()}>
+ {isEditing && (
+
{
+ setIsEditing(false);
+ }}
+ onCancel={() => {
+ setIsEditing(false);
+ }}
+ pin={pin}
+ />
+ )}
+
+ {/* HEADER */}
+
+
+
+ {pin?.pinTags?.map((pt) => pt.tag.title).join(", ")}
+
+
{pin?.title}
+
+
+
+ {sessionData?.user?.id === pin?.ownerId && (
+
{
+ setIsEditing(true);
+ }}
+ title="Edit Waypoint"
+ >
+
+
+ )}
+
+
+
+
+
+
+
+ {/* HORIZONTAL BENTO GALLERY OR FALLBACK */}
+ {pin?.images && pin.images.length > 0 ? (
+
+ {pin.images.map((img) => (
+
+
+
+ ))}
+
+ ) : (
+
+
+
NO IMAGES AVAILABLE
+
+ )}
+
+ {/* BODY: INTEL DASHBOARD */}
+
+
{pin?.description}
+
+
+ {/* PIN ID */}
+
+ PIN ID
+
+ {pin?.id?.padStart(7, "0")}
+
+
+ {/* OWNER */}
+
+ CREATED BY
+ {pin?.owner}
+
+ {/* COORDINATES */}
+
+ COORDINATES (LAT, LNG)
+
+ {pin?.latitude?.toFixed(6)}, {pin?.longitude?.toFixed(6)}
+
+
+ {/* STATUS */}
+
+ STATUS
+
+ {statusData.icon}
+ {statusData.text}
+
+
+ {/* TIMESTAMPS */}
+
+ CREATED AT
+
+ {new Date(pin?.createdAt || "").toLocaleString("default", {
+ month: "long",
+ year: "numeric",
+ day: "2-digit",
+ })}
+
+
+
+ {pin.modifications.filter((m) => m.status === "APPLIED").length >
+ 0 && (
+
+ LAST UPDATED
+
+
+ {new Date(pin?.updatedAt || "").toLocaleString("default", {
+ month: "long",
+ year: "numeric",
+ day: "2-digit",
+ })}{" "}
+ by {pin.modifications[0]?.user.name}
+
+
+ )}
+
+
+ {/* OWNER ACTIONS */}
+ {!isDeleting && sessionData?.user.id === pin?.ownerId && (
+
setIsDeleting(true)}
+ >
+ DELETE PIN
+
+ )}
+
+ {isDeleting && (
+
+
Are you sure you want to permanently delete this pin?
+
+
+ CONFIRM DELETE
+
+ setIsDeleting(false)}
+ >
+ CANCEL
+
+
+
+ )}
+
+
+
FORUM
+ {!isReplying ? (
+ sessionData && (
+
setIsReplying(true)}
+ >
+ + ADD COMMENT
+
+ )
+ ) : (
+
+ )}
+
+ {pin?.comments?.map((thread) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
-
- );
-}
\ No newline at end of file
+
+ );
+}
diff --git a/apps/web/components/PinDetailsCard.tsx b/apps/web/components/PinDetailsCard.tsx
index de6e01e..796c75f 100644
--- a/apps/web/components/PinDetailsCard.tsx
+++ b/apps/web/components/PinDetailsCard.tsx
@@ -18,13 +18,27 @@ export function PinDetailsCard({
onClose,
onExpand,
}: PinDetailsCardProps) {
- const { data: pin } = trpc.pin.getById.useQuery(
- { id: pinId },
- { refetchOnWindowFocus: false },
- );
+ const { data: pin, isLoading: isPinLoading } =
+ trpc.pin.getSimpleById.useQuery(
+ { id: pinId },
+ { refetchOnWindowFocus: false },
+ );
const color = getPinColor(
- pin?.pinTags ? pin?.pinTags[0]?.tag.title || "" : "",
- );
+ pin?.pinTags ? pin?.pinTags[0]?.tag.title || "" : "",
+ );
+
+ if (isPinLoading)
+ return (
+
+ );
return (
@@ -94,8 +108,6 @@ export function PinDetailsCard({
display: flex;
flex-direction: column;
gap: 16px;
- animation: slideUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
- pointer-events: auto;
}
.card-header {
@@ -191,4 +203,4 @@ export function PinDetailsCard({
`}
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/components/TopBar.tsx b/apps/web/components/TopBar.tsx
index e697a04..af610fa 100644
--- a/apps/web/components/TopBar.tsx
+++ b/apps/web/components/TopBar.tsx
@@ -2,12 +2,15 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
-import { trpc } from "@/lib/trpc";
import { useTheme } from "@/lib/ThemeContext";
import { useSession } from "@/lib/auth-client";
import { useMap } from "@vis.gl/react-google-maps";
import { JEEPNEY_ROUTES, ZONE_CATEGORIES } from "@/data/map-layers";
-import { PIN_CATEGORIES, type FilterType, getPinColor } from "@/data/pin-categories";
+import {
+ PIN_CATEGORIES,
+ type FilterType,
+ getPinColor,
+} from "@/data/pin-categories";
interface TopBarProps {
onMenuClick: () => void;
@@ -15,12 +18,12 @@ interface TopBarProps {
onFilterChange: (filter: FilterType) => void;
searchQuery: string;
onSearchChange: (query: string) => void;
- activeRoutes?: string[];
- onToggleRoute?: (routeId: string) => void;
+ activeRoutes?: string[];
+ onToggleRoute?: (routeId: string) => void;
activeZoneCategories?: string[];
- onToggleZoneCategory?: (categoryId: string) => void;
+ onToggleZoneCategory?: (categoryId: string) => void;
userLocation?: { lat: number; lng: number };
- hideControls?: boolean;
+ hideControls?: boolean;
}
export type { FilterType };
@@ -32,11 +35,11 @@ export function TopBar({
searchQuery,
onSearchChange,
activeRoutes = [],
- onToggleRoute = () => {},
+ onToggleRoute = () => {},
activeZoneCategories = [],
- onToggleZoneCategory = () => {},
+ onToggleZoneCategory = () => {},
userLocation = { lat: 14.6549, lng: 121.0645 },
- hideControls = false,
+ hideControls = false,
}: TopBarProps) {
const router = useRouter();
const { data: sessionData } = useSession();
@@ -46,26 +49,26 @@ export function TopBar({
const map = useMap();
- const handleCenterMap = () => {
- if (map && userLocation) {
- map.panTo(userLocation);
- map.setZoom(19);
- }
- };
+ const handleCenterMap = () => {
+ if (map && userLocation) {
+ map.panTo(userLocation);
+ map.setZoom(19);
+ }
+ };
- const handleZoomIn = () => {
- if (map) {
- const currentZoom = map.getZoom() || 19;
- map.setZoom(currentZoom + 1);
- }
- };
+ const handleZoomIn = () => {
+ if (map) {
+ const currentZoom = map.getZoom() || 19;
+ map.setZoom(currentZoom + 1);
+ }
+ };
- const handleZoomOut = () => {
- if (map) {
- const currentZoom = map.getZoom() || 19;
- map.setZoom(currentZoom - 1);
- }
- };
+ const handleZoomOut = () => {
+ if (map) {
+ const currentZoom = map.getZoom() || 19;
+ map.setZoom(currentZoom - 1);
+ }
+ };
const handleProfileClick = () => {
if (sessionData?.user) {
@@ -75,123 +78,155 @@ export function TopBar({
}
};
- const filters: FilterType[] = ["all", ...PIN_CATEGORIES.map(c => c.id as FilterType)];
+ const filters: FilterType[] = [
+ "all",
+ ...PIN_CATEGORIES.map((c) => c.id as FilterType),
+ ];
return (
{/* === LEFT ZONE === */}
-
-
-
-
+
+
+
+
-
-
setIsTransitMenuOpen(!isTransitMenuOpen)}
- >
-
-
+
+
setIsTransitMenuOpen(!isTransitMenuOpen)}
+ >
+
+
- {/* Extruding Route Nodes */}
- {isTransitMenuOpen && (
-
- {JEEPNEY_ROUTES.map((route) => {
- const isActive = activeRoutes.includes(route.id);
- const initial = route.name.replace("UP ", "").charAt(0).toUpperCase();
+ {/* Extruding Route Nodes */}
+ {isTransitMenuOpen && (
+
+ {JEEPNEY_ROUTES.map((route) => {
+ const isActive = activeRoutes.includes(route.id);
+ const initial = route.name
+ .replace("UP ", "")
+ .charAt(0)
+ .toUpperCase();
- return (
- onToggleRoute(route.id)}
- className="route-node"
- title={route.name}
- style={{
- backgroundColor: isActive ? `${route.color}20` : 'var(--bg-panel-hover)',
- color: isActive ? route.color : '#aaa',
- borderColor: isActive ? route.color : 'transparent',
- boxShadow: isActive ? `0 0 10px ${route.color}40` : 'none',
- }}
- >
- {initial}
-
- );
- })}
-
- )}
-
+ return (
+
onToggleRoute(route.id)}
+ className="route-node"
+ title={route.name}
+ style={{
+ backgroundColor: isActive
+ ? `${route.color}20`
+ : "var(--bg-panel-hover)",
+ color: isActive ? route.color : "#aaa",
+ borderColor: isActive ? route.color : "transparent",
+ boxShadow: isActive
+ ? `0 0 10px ${route.color}40`
+ : "none",
+ }}
+ >
+ {initial}
+
+ );
+ })}
+
+ )}
+
-
{
- setIsZoneMenuOpen(!isZoneMenuOpen);
- }}
- >
-
-
+
{
+ setIsZoneMenuOpen(!isZoneMenuOpen);
+ }}
+ >
+
+
- {isZoneMenuOpen && (
-
- {ZONE_CATEGORIES.map((category) => {
- const isActive = activeZoneCategories.includes(category.id);
+ {isZoneMenuOpen && (
+
+ {ZONE_CATEGORIES.map((category) => {
+ const isActive = activeZoneCategories.includes(category.id);
- return (
- onToggleZoneCategory(category.id)}
- className="route-node"
- title={category.label}
- style={{
- backgroundColor: isActive ? `${category.color}20` : 'var(--bg-panel-hover)',
- color: isActive ? category.color : '#aaa',
- borderColor: isActive ? category.color : 'transparent',
- boxShadow: isActive ? `0 0 10px ${category.color}40` : 'none',
- }}
- >
- {category.initial}
-
- );
- })}
-
- )}
-
-
+ return (
+
onToggleZoneCategory(category.id)}
+ className="route-node"
+ title={category.label}
+ style={{
+ backgroundColor: isActive
+ ? `${category.color}20`
+ : "var(--bg-panel-hover)",
+ color: isActive ? category.color : "#aaa",
+ borderColor: isActive ? category.color : "transparent",
+ boxShadow: isActive
+ ? `0 0 10px ${category.color}40`
+ : "none",
+ }}
+ >
+ {category.initial}
+
+ );
+ })}
+
+ )}
+
+
{/* === CENTER ZONE (Search + Filters) === */}
@@ -221,27 +256,27 @@ export function TopBar({
- {filters.map((filter) => {
- const color = getPinColor(filter);
- const isActive = activeFilter === filter;
- return (
- onFilterChange(filter)}
- className={`filter-chip ${isActive ? "active" : ""}`}
- style={{
- borderColor: isActive ? color : "var(--border-color)",
- color: isActive ? "var(--bg-base)" : color,
- backgroundColor: isActive ? color : "var(--bg-panel)",
- transform: isActive ? "scale(1.1)" : "scale(1)",
- }}
- >
- {filter.toUpperCase()}
-
- );
- })}
-
+ {filters.map((filter) => {
+ const color = getPinColor(filter);
+ const isActive = activeFilter === filter;
+ return (
+ onFilterChange(filter)}
+ className={`filter-chip ${isActive ? "active" : ""}`}
+ style={{
+ borderColor: isActive ? color : "var(--border-color)",
+ color: isActive ? "var(--bg-base)" : color,
+ backgroundColor: isActive ? color : "var(--bg-panel)",
+ transform: isActive ? "scale(1.1)" : "scale(1)",
+ }}
+ >
+ {filter.toUpperCase()}
+
+ );
+ })}
+
{/* === RIGHT ZONE (Full Height Tool Stack) === */}
@@ -270,13 +305,35 @@ export function TopBar({
-
+
{theme === "dark" ? (
-