Skip to content

Commit

Permalink
feat(ui): initial code for workspace page [2024-10-31]
Browse files Browse the repository at this point in the history
  • Loading branch information
zerj9 committed Oct 31, 2024
1 parent 641c4f0 commit 989dc3a
Show file tree
Hide file tree
Showing 15 changed files with 1,967 additions and 97 deletions.
653 changes: 652 additions & 1 deletion gridwalk-ui/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions gridwalk-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.453.0",
Expand Down
47 changes: 47 additions & 0 deletions gridwalk-ui/src/app/api/profile/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export async function GET(request) {
try {
// Get the cookies from the request
const cookies = request.headers.get('cookie');
if (!cookies) {
throw new Error('No cookies present');
}

// Parse cookies string to get sid value
const sidCookie = cookies.split(';')
.find(cookie => cookie.trim().startsWith('sid='));

if (!sidCookie) {
throw new Error('Session ID cookie not found');
}

// Extract the sid value
const sidValue = sidCookie.split('=')[1].trim();

// Make the profile request with the sid as bearer token
const response = await fetch('http://localhost:3001/profile', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sidValue}`
}
});

if (!response.ok) {
throw new Error('Profile retrieval failed');
}

const data = await response.json();

return new Response(JSON.stringify({ success: true, data }), {
status: 200,
});
} catch (error) {
return new Response(
JSON.stringify({
success: false,
error: 'Profile retrieval failed.'
}),
{ status: 401 }
);
}
}
16 changes: 16 additions & 0 deletions gridwalk-ui/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ body {
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 222.2 84% 4.9%;
Expand All @@ -59,6 +67,14 @@ body {
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}

Expand Down
68 changes: 68 additions & 0 deletions gridwalk-ui/src/app/workspace/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client'
import React, { useEffect, useState } from 'react';
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "./workspace-sidebar";
import { Menu } from "lucide-react";

export default function Layout({ children }: { children: React.ReactNode }) {
const [profileData, setProfileData] = useState({
first_name: '',
email: '[email protected]', // fallback default
avatar_url: '/path/to/avatar.jpg' // fallback default
});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchProfile = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/profile');

if (!response.ok) {
throw new Error('Failed to fetch profile');
}

const data = await response.json();
setProfileData({
first_name: data.first_name,
email: data.email || profileData.email,
avatar_url: data.avatar_url || profileData.avatar_url
});
} catch (error) {
console.error('Error fetching profile:', error);
setError(error instanceof Error ? error.message : 'Failed to load profile');
} finally {
setIsLoading(false);
}
};

fetchProfile();
}, []); // Empty dependency array means this runs once on mount

return (
<SidebarProvider>
<div className="flex h-screen">
<AppSidebar
userName={profileData.first_name}
userEmail={profileData.email}
avatarUrl={profileData.avatar_url}
/>
<main className="flex-1 px-4 overflow-auto bg-gray-50">
<div className="p-4">
<SidebarTrigger className="lg:hidden">
<Menu className="h-5 w-5" />
</SidebarTrigger>
{isLoading ? (
<div>Loading profile...</div>
) : error ? (
<div>Error: {error}</div>
) : (
children
)}
</div>
</main>
</div>
</SidebarProvider>
);
}
153 changes: 57 additions & 96 deletions gridwalk-ui/src/app/workspace/page.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,62 @@
import Image from "next/image";
import React from 'react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';

export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
export default function Workspace() {
const projects = [
{
title: "Downtown District Mapping",
description: "Detailed street-level mapping of the central business district",
status: "In Progress",
},
{
title: "Parks & Recreation Zones",
description: "Comprehensive mapping of public recreational areas and green spaces",
status: "Planning",
},
{
title: "Historic Districts Survey",
description: "Documentation and mapping of heritage sites and landmarks",
status: "Not Started",
}
];

<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
return (
<div className="mt-4">
<div className="mb-8">
<h1 className="text-4xl font-bold text-slate-900 mb-2">
Map Projects
</h1>
<p className="text-slate-600 text-lg font-medium">
Geographic information systems and mapping initiatives
</p>
</div>

<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project, index) => (
<Card key={index} className="group hover:shadow-lg transition-all duration-300">
<div className="aspect-video w-full relative overflow-hidden">
<img
src="/api/placeholder/400/225"
alt={`Map preview for ${project.title}`}
className="object-cover w-full h-full group-hover:scale-105 transition-transform duration-300"
/>
<div className="absolute top-2 right-2">
<span className="bg-white/90 backdrop-blur-sm text-slate-700 px-3 py-1 rounded-full text-sm font-medium">
{project.status}
</span>
</div>
</div>
<CardHeader className="space-y-2">
<CardTitle className="text-xl font-bold text-slate-900 line-clamp-1">
{project.title}
</CardTitle>
<CardDescription className="text-slate-600 font-medium line-clamp-2">
{project.description}
</CardDescription>
</CardHeader>
</Card>
))}
</div>
</div>
);
}
65 changes: 65 additions & 0 deletions gridwalk-ui/src/app/workspace/workspace-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import { Sidebar, SidebarContent } from "@/components/ui/sidebar";
import { Database, Map } from "lucide-react";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";

interface AppSidebarProps {
userName?: string;
userEmail?: string;
avatarUrl?: string;
}

const navItems = [
{ icon: Map, label: 'Projects', href: '/workspace' },
{ icon: Database, label: 'Connections', href: '/workspace' },
];

export function AppSidebar({
userName = "John Doe",
userEmail = "[email protected]",
avatarUrl = "/api/placeholder/32/32"
}: AppSidebarProps) {
// Get first letter of name for avatar fallback
const avatarFallback = userName.charAt(0).toUpperCase();

return (
<Sidebar className="">
<SidebarContent>
<div className="flex h-full flex-col">
{/* Logo */}
<div className="px-4 py-6">
<h1 className="text-xl font-bold">Workspace</h1>
</div>

{/* Navigation */}
<nav className="flex-1 space-y-1 px-2">
{navItems.map((item) => (
<a
key={item.label}
href={item.href}
className="flex items-center rounded-lg px-3 py-2 text-sm font-medium hover:bg-gray-100"
>
<item.icon className="mr-3 h-5 w-5" />
{item.label}
</a>
))}
</nav>

{/* User Profile */}
<div className="border-t p-4">
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage src={avatarUrl} alt={userName} />
<AvatarFallback>{avatarFallback}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium">{userName}</p>
<p className="text-xs text-gray-500">{userEmail}</p>
</div>
</div>
</div>
</div>
</SidebarContent>
</Sidebar>
);
}
Loading

0 comments on commit 989dc3a

Please sign in to comment.