From 772bda4d22f24bd3afd9b8a3accf02d68f6eed0c Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 11:59:33 +0800 Subject: [PATCH 01/21] feat: make map center,. zoom in, and zoom out buttons functional --- apps/web/app/page.tsx | 30 +++++++++++-- apps/web/components/TopBar.tsx | 82 +++++++++++++++++----------------- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index f2b34a0..ce4c46a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -69,6 +69,30 @@ export default function Home() { }); }, []); + const mockUserLocation = { lat: 14.6549, lng: 121.0645 }; + const mockHeading = 45; + + const handleCenterMap = useCallback(() => { + setCameraProps({ + center: mockUserLocation, + zoom: 19, + }); + }, [mockUserLocation]); + + const handleZoomIn = useCallback(() => { + setCameraProps((prev) => ({ + ...prev, + zoom: Math.min(prev.zoom + 1, 22) + })); + }, []); + + const handleZoomOut = useCallback(() => { + setCameraProps((prev) => ({ + ...prev, + zoom: Math.max(prev.zoom - 1, 17) + })); + }, []); + const { theme } = useTheme(); const [pendingPinCoords, setPendingPinCoords] = useState<{ @@ -102,9 +126,6 @@ export default function Home() { return () => window.removeEventListener("mousemove", handleMouseMove); }, [isAddingPin]); - const mockUserLocation = { lat: 14.6549, lng: 121.0645 }; - const mockHeading = 45; - return (
{/* TARGETING CROSSHAIR (Only visible when armed) */} diff --git a/apps/web/components/TopBar.tsx b/apps/web/components/TopBar.tsx index 29116ea..b43bdf0 100644 --- a/apps/web/components/TopBar.tsx +++ b/apps/web/components/TopBar.tsx @@ -19,6 +19,9 @@ interface TopBarProps { onToggleRoute?: (routeId: string) => void; activeZoneCategories?: string[]; onToggleZoneCategory?: (categoryId: string) => void; + onCenterMap?: () => void; + onZoomIn?: () => void; + onZoomOut?: () => void; } export function TopBar({ @@ -31,6 +34,9 @@ export function TopBar({ onToggleRoute = () => {}, activeZoneCategories = [], onToggleZoneCategory = () => {}, + onCenterMap = () => {}, + onZoomIn = () => {}, + onZoomOut = () => {}, }: TopBarProps) { const router = useRouter(); const { data: sessionData } = useSession(); @@ -263,48 +269,42 @@ export function TopBar({ {/* Bottom Group */}
- + -
- -
- -
-
+
+ +
+ +
+ ); From 31e62577bcebbea5c1cace7354b73e5ad8c0df40 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 12:11:05 +0800 Subject: [PATCH 02/21] feat: use `react-google-maps` instead for zooming instead --- apps/web/app/page.tsx | 25 +--------------------- apps/web/components/TopBar.tsx | 38 ++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index ce4c46a..f7a2930 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -72,27 +72,6 @@ export default function Home() { const mockUserLocation = { lat: 14.6549, lng: 121.0645 }; const mockHeading = 45; - const handleCenterMap = useCallback(() => { - setCameraProps({ - center: mockUserLocation, - zoom: 19, - }); - }, [mockUserLocation]); - - const handleZoomIn = useCallback(() => { - setCameraProps((prev) => ({ - ...prev, - zoom: Math.min(prev.zoom + 1, 22) - })); - }, []); - - const handleZoomOut = useCallback(() => { - setCameraProps((prev) => ({ - ...prev, - zoom: Math.max(prev.zoom - 1, 17) - })); - }, []); - const { theme } = useTheme(); const [pendingPinCoords, setPendingPinCoords] = useState<{ @@ -258,9 +237,7 @@ export default function Home() { onToggleRoute={handleToggleRoute} activeZoneCategories={activeZoneCategories} onToggleZoneCategory={handleToggleZoneCategory} - onCenterMap={handleCenterMap} - onZoomIn={handleZoomIn} - onZoomOut={handleZoomOut} + userLocation={mockUserLocation} /> {/* TARGETING CROSSHAIR (Only visible when armed) */} diff --git a/apps/web/components/TopBar.tsx b/apps/web/components/TopBar.tsx index b43bdf0..fdd1a64 100644 --- a/apps/web/components/TopBar.tsx +++ b/apps/web/components/TopBar.tsx @@ -5,6 +5,7 @@ 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"; export type FilterType = "all" | "academic" | "food" | "social" | "utility"; @@ -19,9 +20,7 @@ interface TopBarProps { onToggleRoute?: (routeId: string) => void; activeZoneCategories?: string[]; onToggleZoneCategory?: (categoryId: string) => void; - onCenterMap?: () => void; - onZoomIn?: () => void; - onZoomOut?: () => void; + userLocation?: { lat: number; lng: number }; } export function TopBar({ @@ -34,9 +33,7 @@ export function TopBar({ onToggleRoute = () => {}, activeZoneCategories = [], onToggleZoneCategory = () => {}, - onCenterMap = () => {}, - onZoomIn = () => {}, - onZoomOut = () => {}, + userLocation = { lat: 14.6549, lng: 121.0645 }, }: TopBarProps) { const router = useRouter(); const { data: sessionData } = useSession(); @@ -44,6 +41,29 @@ export function TopBar({ const [isZoneMenuOpen, setIsZoneMenuOpen] = useState(false); const { theme, toggleTheme } = useTheme(); + const map = useMap(); + + 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 handleZoomOut = () => { + if (map) { + const currentZoom = map.getZoom() || 19; + map.setZoom(currentZoom - 1); + } + }; + const handleProfileClick = () => { if (sessionData?.user) { router.push("/dashboard"); @@ -272,7 +292,7 @@ export function TopBar({ + + +
+ {/* PRIMARY ACTIONS */} +
+ SYSTEM ACCESS + +
+ + {/* SETTINGS */} +
+ DISPLAY SETTINGS + +
+ + {/* PLACEHOLDERS */} +
+ DATABASE + + +
+
+ +
+ UP WAYPOINT v1.3.0 + SYSTEM ONLINE +
+ + + + + ); +} \ No newline at end of file From a6639754890bcd9269690c1d6ab565fa3b7393bb Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 12:43:40 +0800 Subject: [PATCH 05/21] fix: snap top left buttons to bottom right when on mobile to reduce collision --- apps/web/app/globals.css | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 2b708bd..ec792a2 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -308,7 +308,23 @@ p, .font-desc, .details-card p { } .lock-button:active { transform: scale(0.95); } -@media (max-width: 768px) { .profile-btn, .theme-toggle { display: none; } } +@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; + } +} @keyframes slideUp { from { transform: translateY(100px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } From 7f68d0569ddc32780b26549fda7abbcf74da1754 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 12:44:24 +0800 Subject: [PATCH 06/21] chore: format `globals.css` --- apps/web/app/globals.css | 292 ++++++++++++++++++++++++++------------- 1 file changed, 193 insertions(+), 99 deletions(-) diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index ec792a2..805000c 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -3,25 +3,25 @@ @tailwind utilities; :root { - --bg-base: #f0f2f5; - --bg-panel: rgba(255, 255, 255, 0.9); + --bg-base: #f0f2f5; + --bg-panel: rgba(255, 255, 255, 0.9); --bg-panel-hover: rgba(0, 0, 0, 0.05); - + --text-primary: #121212; --text-secondary: #555555; - + --border-color: rgba(0, 0, 0, 0.15); --shadow-glow: rgba(0, 136, 255, 0.2); } .dark { - --bg-base: #0f1115; - --bg-panel: rgba(10, 10, 12, 0.85); + --bg-base: #0f1115; + --bg-panel: rgba(10, 10, 12, 0.85); --bg-panel-hover: rgba(255, 255, 255, 0.1); - + --text-primary: #ffffff; --text-secondary: #8899A6; - + --border-color: rgba(255, 255, 255, 0.15); --shadow-glow: rgba(0, 229, 255, 0.2); } @@ -36,7 +36,9 @@ body { font-family: var(--font-chakra), sans-serif; } -p, .font-desc, .details-card p { +p, +.font-desc, +.details-card p { font-family: var(--font-nunito), sans-serif; } @@ -111,18 +113,28 @@ p, .font-desc, .details-card p { } /* --- ZONES --- */ -.zone-left, .zone-right { - pointer-events: auto; +.zone-left, +.zone-right { + pointer-events: auto; } .zone-left { - position: absolute; top: 8px; left: 8px; z-index: 20; + 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; + position: absolute; + top: 8px; + right: 8px; + bottom: 24px; + display: flex; + flex-direction: column; + justify-content: space-between; + width: 44px; + z-index: 20; } .zone-center { @@ -132,18 +144,18 @@ p, .font-desc, .details-card p { transform: translateX(-50%); width: 100%; max-width: 600px; - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; align-items: center; - gap: 8px; + gap: 8px; pointer-events: none; z-index: 10; } /* --- CENTER CONTENTS --- */ .search-block { - position: relative; - width: 100%; + position: relative; + width: 100%; max-width: 350px; margin: 0 auto; pointer-events: auto; @@ -151,10 +163,13 @@ p, .font-desc, .details-card p { display: flex; align-items: center; } -.search-block:focus-within { transform: scale(1.05); } + +.search-block:focus-within { + transform: scale(1.05); +} .search-input { - width: 100%; + width: 100%; height: 44px; background: var(--bg-panel); backdrop-filter: blur(12px); @@ -162,129 +177,168 @@ p, .font-desc, .details-card p { border-radius: 14px; box-sizing: border-box; padding: 0 40px 0 14px; - color: var(--text-primary); font-size: 13px; font-weight: 700; + 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-input:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.3); +} .search-icon-right { - position: absolute; - right: 12px; + position: absolute; + right: 12px; top: 50%; - transform: translateY(-50%); + transform: translateY(-50%); color: #aaa; pointer-events: none; z-index: 5; } .filter-row { - display: flex; + display: flex; justify-content: center; - gap: 8px; - width: 100%; + gap: 8px; + width: 100%; overflow-x: auto; - padding-top: 4px; + padding-top: 4px; padding-bottom: 4px; pointer-events: auto; } -.no-scrollbar::-webkit-scrollbar { display: none; } + +.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; + 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; +.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; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; transition: transform 0.1s; font-family: var(--font-chakra); } -.icon-button:active, .control-button:active { +.icon-button:active, +.control-button:active { transform: scale(0.92); } -.tool-group { display: flex; flex-direction: column; gap: 8px; } -.bottom-align { margin-top: auto; } +.tool-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.bottom-align { + margin-top: auto; +} .zoom-stack { - display: flex; flex-direction: column; + display: flex; + flex-direction: column; background: var(--bg-panel); backdrop-filter: blur(12px); border: 1px solid var(--border-color); - border-radius: 12px; overflow: hidden; + border-radius: 12px; + overflow: hidden; +} + +.control-button { + border: none; + border-radius: 0; +} + +.divider { + height: 1px; + background: var(--border-color); + width: 80%; + margin: 0 auto; } -.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; + 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); + 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; + 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; + 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; + 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); - } + from { + opacity: 0; + transform: translateX(-15px) scale(0.9); + } + + to { + opacity: 1; + transform: translateX(0) scale(1); + } } /* --- HUD & ANIMATIONS --- */ @@ -294,7 +348,8 @@ p, .font-desc, .details-card p { border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 20px; padding: 24px; - width: 100%; max-width: 400px; + width: 100%; + max-width: 400px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); animation: slideUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); } @@ -306,26 +361,65 @@ p, .font-desc, .details-card p { cursor: pointer; transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } -.lock-button:active { transform: scale(0.95); } + +.lock-button:active { + transform: scale(0.95); +} @media (max-width: 768px) { - .profile-btn, .theme-toggle { - display: none; + + .profile-btn, + .theme-toggle { + display: none; } .zone-left { - top: auto; - bottom: 24px; - display: flex; - flex-direction: column-reverse; - gap: 12px; + top: auto; + bottom: 24px; + display: flex; + flex-direction: column-reverse; + gap: 12px; } .zone-left .transit-system-container { - margin-top: 0; + margin-top: 0; + } +} + +@keyframes slideUp { + from { + transform: translateY(100px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; } } -@keyframes slideUp { from { transform: translateY(100px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } -@keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } -@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } } \ No newline at end of file +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes pulse { + 0% { + opacity: 1; + } + + 50% { + opacity: 0.4; + } + + 100% { + opacity: 1; + } +} \ No newline at end of file From 3b9d7b7ac9d5b7035bb10013a8771e9191acb177 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 12:54:06 +0800 Subject: [PATCH 07/21] fix: use `nowrap` for meta values --- apps/web/components/ExpandedPinView.tsx | 574 ++++++++++++------------ 1 file changed, 296 insertions(+), 278 deletions(-) diff --git a/apps/web/components/ExpandedPinView.tsx b/apps/web/components/ExpandedPinView.tsx index 88969fe..a3adbb1 100644 --- a/apps/web/components/ExpandedPinView.tsx +++ b/apps/web/components/ExpandedPinView.tsx @@ -10,103 +10,103 @@ import { useForm } from "react-hook-form"; import z from "zod"; 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 && ( - - ) - ) : ( -
- 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 && ( + + ) + ) : ( + + - - - - )} -
- - {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 = getFilterColor( - pin?.pinTags ? pin.pinTags[0]?.tag.title || "" : "", - ); - - return ( -
-
e.stopPropagation()}> -
- {/* HEADER */} -
-
- - {pin?.pinTags?.map((pt) => pt.tag.title).join(", ")} - -

{pin?.title}

-
- -
- - {/* HORIZONTAL BENTO GALLERY */} -
- {pin?.images?.map((img) => ( -
- -
- ))} -
- - {/* 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 - - {pin?.status} - -
- - {/* 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 && ( - - )} - - {isDeleting && ( -
-

Are you sure you want to permanently delete this pin?

-
- - -
-
- )} - -
-

FORUM

- {!isReplying ? ( - sessionData && ( - - ) - ) : ( -
- +
e.stopPropagation()}> +
+ {/* HEADER */} +
+
+ + {pin?.pinTags?.map((pt) => pt.tag.title).join(", ")} + +

{pin?.title}

+
+ +
+ + {/* HORIZONTAL BENTO GALLERY */} +
+ {pin?.images?.map((img) => ( +
+ +
+ ))} +
+ + {/* 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 + + {pin?.status} + +
+ + {/* 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 && ( + + )} + + {isDeleting && ( +
+

Are you sure you want to permanently delete this pin?

+
+ + +
+
+ )} + +
+

FORUM

+ {!isReplying ? ( + sessionData && ( + + ) + ) : ( + + - - - - )} -
- {pin?.comments?.map((thread) => ( - - ))} -
-
-
-
- -
-
- - -
- ); +
+ ); } \ No newline at end of file From fd70e3b3a24b96f8ee5b66e40e1652c79555ad5e Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 12:58:01 +0800 Subject: [PATCH 08/21] feat: auto-scroll for long meta values --- apps/web/components/ExpandedPinView.tsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/web/components/ExpandedPinView.tsx b/apps/web/components/ExpandedPinView.tsx index a3adbb1..06c7046 100644 --- a/apps/web/components/ExpandedPinView.tsx +++ b/apps/web/components/ExpandedPinView.tsx @@ -440,12 +440,26 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { display: flex; flex-direction: column; gap: 4px; - min-width: 0; /* Prevents long text from blowing out the flex/grid columns */ + min-width: 0; + /* 1. Turn the container into a measurable query zone */ + container-type: inline-size; + /* 2. Hide the overflow from bleeding into other columns */ + overflow: hidden; } .col-span-2 { grid-column: span 2; } .meta-label { font-family: var(--font-chakra); font-size: 10px; font-weight: 800; color: var(--text-secondary); letter-spacing: 0.1em; } + .meta-value { + font-family: var(--font-nunito); + font-size: 14px; + font-weight: 700; + color: var(--text-primary); + white-space: nowrap; + width: max-content; + animation: autoPan 6s ease-in-out infinite alternate; + } + .meta-value { font-family: var(--font-nunito); font-size: 14px; @@ -509,6 +523,14 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes scalePop { from { opacity: 0; transform: scale(0.95) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } + @keyframes autoPan { + 0%, 20% { + transform: translateX(0); + } + 80%, 100% { + transform: translateX(min(0px, calc(100cqw - 100%))); + } + } `}
); From 2132cbc83520b27e3876fd9101e065cc5ef0b4da Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 13:06:22 +0800 Subject: [PATCH 09/21] fix: indicate "NO IMAGES AVAILABLE" for pins with no images uploaded yet --- apps/web/components/ExpandedPinView.tsx | 55 +++++++++++++++++++++---- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/apps/web/components/ExpandedPinView.tsx b/apps/web/components/ExpandedPinView.tsx index 06c7046..881ed62 100644 --- a/apps/web/components/ExpandedPinView.tsx +++ b/apps/web/components/ExpandedPinView.tsx @@ -215,14 +215,36 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) {
- {/* HORIZONTAL BENTO GALLERY */} -
- {pin?.images?.map((img) => ( -
- -
- ))} -
+ {/* HORIZONTAL BENTO GALLERY OR FALLBACK */} + {pin?.images && pin.images.length > 0 ? ( +
+ {pin.images.map((img) => ( +
+ +
+ ))} +
+ ) : ( +
+ + + + + + + NO IMAGES AVAILABLE +
+ )} {/* BODY: INTEL DASHBOARD */}
@@ -426,6 +448,23 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { } .photo-placeholder.large { grid-row: span 2; width: 192px; } + .no-photo-placeholder { + height: 192px; + margin-bottom: 24px; + background: var(--bg-panel-hover); + border: 1px dashed var(--border-color); + border-radius: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: var(--text-secondary); + font-family: var(--font-chakra); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.15em; + } + /* Body Data */ .modal-body { display: flex; flex-direction: column; gap: 24px; } .description { font-family: var(--font-nunito); font-size: 15px; color: var(--text-primary); line-height: 1.5; margin: 0; } From 782ed72950245725f1003c5813c35ff21ea8bac3 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 13:33:00 +0800 Subject: [PATCH 10/21] fix: include `transit` in `TopBar` --- apps/web/components/TopBar.tsx | 43 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/web/components/TopBar.tsx b/apps/web/components/TopBar.tsx index fdd1a64..8f9514d 100644 --- a/apps/web/components/TopBar.tsx +++ b/apps/web/components/TopBar.tsx @@ -8,7 +8,7 @@ import { useSession } from "@/lib/auth-client"; import { useMap } from "@vis.gl/react-google-maps"; import { JEEPNEY_ROUTES, ZONE_CATEGORIES } from "@/data/map-layers"; -export type FilterType = "all" | "academic" | "food" | "social" | "utility"; +export type FilterType = "all" | "academic" | "food" | "social" | "transit" | "utility"; interface TopBarProps { onMenuClick: () => void; @@ -73,12 +73,13 @@ export function TopBar({ }; const filters: FilterType[] = [ - "all", - "academic", - "food", - "social", - "utility", - ]; + "all", + "academic", + "food", + "social", + "transit", + "utility", + ]; return (
@@ -331,18 +332,18 @@ export function TopBar({ } export const getFilterColor = (type: string) => { - switch (type) { - case "academic": - return "#ff4d4d"; - case "food": - return "#00ffa3"; - case "social": - return "#ff007a"; - case "transit": - return "#f4ff4d"; - case "utility": - return "#00d1ff"; - default: - return "var(--text-primary)"; - } + switch (type.toLowerCase()) { + case "academic": + return "#FF3366"; + case "food": + return "#00FF99"; + case "social": + return "#B026FF"; + case "transit": + return "#FFD700"; + case "utility": + return "#00E5FF"; + default: + return "var(--text-primary)"; + } }; From 2c2fa611e0344df45dd8e33c7a900d42e8e361cc Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 13:36:53 +0800 Subject: [PATCH 11/21] fix: adjust `search-block` to `zone-center` --- apps/web/app/globals.css | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 805000c..267d4f8 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -142,11 +142,11 @@ p, top: 8px; left: 50%; transform: translateX(-50%); - width: 100%; - max-width: 600px; + width: max-content; + max-width: calc(100vw - 120px); display: flex; flex-direction: column; - align-items: center; + align-items: stretch; gap: 8px; pointer-events: none; z-index: 10; @@ -156,7 +156,6 @@ p, .search-block { position: relative; width: 100%; - max-width: 350px; margin: 0 auto; pointer-events: auto; transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); From f1a72930ce7ee4af85bb15c0e41711e820eff743 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 13:57:13 +0800 Subject: [PATCH 12/21] fix: beautify pin verification status field --- apps/web/components/ExpandedPinView.tsx | 71 ++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/apps/web/components/ExpandedPinView.tsx b/apps/web/components/ExpandedPinView.tsx index 881ed62..22583bd 100644 --- a/apps/web/components/ExpandedPinView.tsx +++ b/apps/web/components/ExpandedPinView.tsx @@ -186,6 +186,59 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { 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()}> @@ -276,8 +329,15 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { {/* STATUS */}
STATUS - - {pin?.status} + + {statusData.icon} + {statusData.text}
@@ -512,6 +572,13 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { display: none; /* Safari and Chrome */ } + .status-badge { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + } + .font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; letter-spacing: 0.05em; } .font-cubao-wide { font-family: var(--font-cubao-wide); font-weight: 100; letter-spacing: 0.1em;} .text-muted { color: var(--text-secondary); font-style: italic; } From b1928ab685a5a6ad8d820c6a6b2d9aa940633097 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 14:09:43 +0800 Subject: [PATCH 13/21] feat: add edit button --- apps/web/components/ExpandedPinView.tsx | 88 ++++++++++++++++++------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/apps/web/components/ExpandedPinView.tsx b/apps/web/components/ExpandedPinView.tsx index 22583bd..d177686 100644 --- a/apps/web/components/ExpandedPinView.tsx +++ b/apps/web/components/ExpandedPinView.tsx @@ -251,21 +251,41 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) {

{pin?.title}

- + +
+ {sessionData?.user?.id === pin?.ownerId && ( + + )} + + +
{/* HORIZONTAL BENTO GALLERY OR FALLBACK */} @@ -478,19 +498,39 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { .custom-scrollbar::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 10px; transition: background 0.2s; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); } - /* Header & Images */ - .modal-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; } + /* Header & Actions */ + .modal-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 20px; + } + + .header-actions { + display: flex; + gap: 8px; + align-items: center; + } + .badge { font-family: var(--font-cubao-wide); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; } h2 { font-family: var(--font-chakra); color: var(--text-primary); font-size: 26px; font-weight: 800; margin: 4px 0 0 0; } - - .close-btn { - background: transparent; border: 1px solid var(--border-color); - border-radius: 50%; width: 44px; height: 44px; color: var(--text-primary); cursor: pointer; - display: flex; align-items: center; justify-content: center; transition: all 0.2s; + + .close-btn, .edit-btn { + background: transparent; + border: 1px solid var(--border-color); + border-radius: 50%; + width: 44px; + height: 44px; + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; flex-shrink: 0; } - .close-btn:hover { background: var(--bg-panel-hover); } - .close-btn:active { transform: scale(0.9); } + .close-btn:hover, .edit-btn:hover { background: var(--bg-panel-hover); } + .close-btn:active, .edit-btn:active { transform: scale(0.9); } .photo-gallery { display: grid; grid-template-rows: repeat(2, 90px); grid-auto-flow: column; From 9d70b9fb7b4d50eafa18c537ea03210b09de30ad Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 14:15:04 +0800 Subject: [PATCH 14/21] fix: hide top left buttons and bottom right buttons when selecting a pin or opening a pin --- apps/web/app/page.tsx | 1 + apps/web/components/TopBar.tsx | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 2fee211..4491f46 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -239,6 +239,7 @@ export default function Home() { activeZoneCategories={activeZoneCategories} onToggleZoneCategory={handleToggleZoneCategory} userLocation={mockUserLocation} + hideControls={!!selectedPinId} /> {/* TARGETING CROSSHAIR (Only visible when armed) */} diff --git a/apps/web/components/TopBar.tsx b/apps/web/components/TopBar.tsx index 8f9514d..fe48568 100644 --- a/apps/web/components/TopBar.tsx +++ b/apps/web/components/TopBar.tsx @@ -21,6 +21,7 @@ interface TopBarProps { activeZoneCategories?: string[]; onToggleZoneCategory?: (categoryId: string) => void; userLocation?: { lat: number; lng: number }; + hideControls?: boolean; } export function TopBar({ @@ -34,6 +35,7 @@ export function TopBar({ activeZoneCategories = [], onToggleZoneCategory = () => {}, userLocation = { lat: 14.6549, lng: 121.0645 }, + hideControls = false, }: TopBarProps) { const router = useRouter(); const { data: sessionData } = useSession(); @@ -84,7 +86,14 @@ export function TopBar({ return (
{/* === LEFT ZONE === */} -
+
- ); - })} -
+ {filters.map((filter) => { + const color = getPinColor(filter); + const isActive = activeFilter === filter; + return ( + + ); + })} +
{/* === RIGHT ZONE (Full Height Tool Stack) === */} @@ -345,21 +339,4 @@ export function TopBar({
); -} - -export const getFilterColor = (type: string) => { - switch (type.toLowerCase()) { - case "academic": - return "#FF3366"; - case "food": - return "#00FF99"; - case "social": - return "#B026FF"; - case "transit": - return "#FFD700"; - case "utility": - return "#00E5FF"; - default: - return "var(--text-primary)"; - } -}; +} \ No newline at end of file From 456c1e1d4c53c633d4c9e5f8917d07fb316c8bad Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 15:51:11 +0800 Subject: [PATCH 17/21] refactor: use `pin-categories` for `ExpandedPinView` --- apps/web/components/ExpandedPinView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/components/ExpandedPinView.tsx b/apps/web/components/ExpandedPinView.tsx index d177686..71d233a 100644 --- a/apps/web/components/ExpandedPinView.tsx +++ b/apps/web/components/ExpandedPinView.tsx @@ -1,6 +1,6 @@ "use client"; -import { getFilterColor } from "@/components/TopBar"; +import { getPinColor } from "@/data/pin-categories"; import { useSession } from "@/lib/auth-client"; import { trpc } from "@/lib/trpc"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -182,7 +182,7 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { deletePin.mutate({ id: pinId }); } - const color = getFilterColor( + const color = getPinColor( pin?.pinTags ? pin.pinTags[0]?.tag.title || "" : "", ); @@ -514,7 +514,7 @@ export function ExpandedPinView({ pinId, onClose }: ExpandedPinViewProps) { .badge { font-family: var(--font-cubao-wide); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; } h2 { font-family: var(--font-chakra); color: var(--text-primary); font-size: 26px; font-weight: 800; margin: 4px 0 0 0; } - + .close-btn, .edit-btn { background: transparent; border: 1px solid var(--border-color); From 5a04c5526f7bda476475b43121b444282ce2a58f Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 15:52:04 +0800 Subject: [PATCH 18/21] refactor: use `pin-categories` for `PinDetailsCard` --- apps/web/components/PinDetailsCard.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/components/PinDetailsCard.tsx b/apps/web/components/PinDetailsCard.tsx index 494fc4f..de6e01e 100644 --- a/apps/web/components/PinDetailsCard.tsx +++ b/apps/web/components/PinDetailsCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { getFilterColor } from "@/components/TopBar"; +import { getPinColor } from "@/data/pin-categories"; import { trpc } from "@/lib/trpc"; interface PinDetailsCardProps { @@ -22,9 +22,9 @@ export function PinDetailsCard({ { id: pinId }, { refetchOnWindowFocus: false }, ); - const color = getFilterColor( - pin?.pinTags ? pin?.pinTags[0]?.tag.title || "" : "", - ); + const color = getPinColor( + pin?.pinTags ? pin?.pinTags[0]?.tag.title || "" : "", + ); return (
From 304bfc82d4ebb89f497d3203705193ee4e449e94 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 15:52:41 +0800 Subject: [PATCH 19/21] refactor: use `pin-categories` for `NeonPin` --- apps/web/components/NeonPin.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/components/NeonPin.tsx b/apps/web/components/NeonPin.tsx index d5a9d7f..d38c3dd 100644 --- a/apps/web/components/NeonPin.tsx +++ b/apps/web/components/NeonPin.tsx @@ -1,5 +1,5 @@ import type { PinRouterOutputs } from "@repo/api"; -import { getFilterColor } from "./TopBar"; +import { getPinColor } from "@/data/pin-categories"; type Pin = PinRouterOutputs["getAll"][number]; interface NeonPinProps { @@ -17,7 +17,7 @@ export function NeonPin({ isVisible, onClick, }: NeonPinProps) { - const color = getFilterColor(pin?.pinTags[0]?.tag.title || ""); + const color = getPinColor(pin?.pinTags[0]?.tag.title || ""); const baseScale = isVisible ? (isSelected ? 1.2 : 1) : 0; const opacity = isVisible ? 1 : 0; From b5979fb21be13405afaff7694d7ae0db726494b8 Mon Sep 17 00:00:00 2001 From: ptrn23 Date: Mon, 23 Mar 2026 16:01:58 +0800 Subject: [PATCH 20/21] refactor: use CSS variables for a darker palette of pins --- apps/web/app/globals.css | 12 ++++++ apps/web/components/AddPinModal.tsx | 65 ++++++++++++++++------------- apps/web/data/pin-categories.ts | 10 ++--- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 267d4f8..eae39d5 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -12,6 +12,12 @@ --border-color: rgba(0, 0, 0, 0.15); --shadow-glow: rgba(0, 136, 255, 0.2); + + --pin-academic: #D91A4D; + --pin-food: #00995C; + --pin-social: #8E00E6; + --pin-transit: #D98200; + --pin-utility: #0088CC; } .dark { @@ -24,6 +30,12 @@ --border-color: rgba(255, 255, 255, 0.15); --shadow-glow: rgba(0, 229, 255, 0.2); + + --pin-academic: #FF3366; + --pin-food: #00FF99; + --pin-social: #B026FF; + --pin-transit: #FFD700; + --pin-utility: #00E5FF; } body { diff --git a/apps/web/components/AddPinModal.tsx b/apps/web/components/AddPinModal.tsx index f6d0d56..1151bd9 100644 --- a/apps/web/components/AddPinModal.tsx +++ b/apps/web/components/AddPinModal.tsx @@ -7,6 +7,7 @@ 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 = Omit; @@ -129,29 +130,40 @@ export function AddPinModal({ coords, onSave, onCancel }: AddPinModalProps) {
- PIN TYPES (select all that apply) -
- {tagsData?.map((t) => ( - - ))} -
-
+ PIN TYPES (select all that apply) +
+ {tagsData?.map((t) => { + const isActive = tags.includes(t.id); + const tagColor = getPinColor(t.title); + + return ( + + ); + })} +
+
Date: Mon, 23 Mar 2026 16:02:50 +0800 Subject: [PATCH 21/21] refactor: use dark pin palette for `AddPinModal` --- apps/web/components/AddPinModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/components/AddPinModal.tsx b/apps/web/components/AddPinModal.tsx index 1151bd9..1fac01a 100644 --- a/apps/web/components/AddPinModal.tsx +++ b/apps/web/components/AddPinModal.tsx @@ -152,10 +152,10 @@ export function AddPinModal({ coords, onSave, onCancel }: AddPinModalProps) { } }} style={isActive ? { - backgroundColor: `${tagColor}25`, + backgroundColor: `color-mix(in srgb, ${tagColor} 25%, transparent)`, borderColor: tagColor, color: tagColor, - boxShadow: `inset 0 0 10px ${tagColor}40` + boxShadow: `inset 0 0 10px color-mix(in srgb, ${tagColor} 40%, transparent)` } : {}} > {t.title.toUpperCase()}