From 1b6a8c7fb749a31958e131d49ab9140e3745b147 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Tue, 19 Aug 2025 08:41:51 +0000 Subject: [PATCH 1/3] feat(sidebar): Implement sidebar component with navigation links and toggle functionality feat(projects): Add ProjectPage component to display project details and integrate with sidebar --- web/components/SideBar.tsx | 83 ++++++++++++++++++++++++++++++++++++++ web/index.tsx | 5 +++ web/pages/ProjectPage.tsx | 25 ++++++++++++ web/pages/ProjectsPage.tsx | 6 +-- 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 web/components/SideBar.tsx create mode 100644 web/pages/ProjectPage.tsx diff --git a/web/components/SideBar.tsx b/web/components/SideBar.tsx new file mode 100644 index 0000000..cc15071 --- /dev/null +++ b/web/components/SideBar.tsx @@ -0,0 +1,83 @@ +import { signal } from '@preact/signals' +import { + ChevronsLeft, + ChevronsRight, + HardDrive, + LucideIcon, + Settings, +} from 'lucide-preact' +import { A, LinkProps, url } from '../lib/router.tsx' + +const isCollapsed = signal(false) + +const NavLink = ( + { icon: Icon, children, current, ...props }: LinkProps & { + current: boolean + icon: LucideIcon + }, +) => ( + + + {!isCollapsed.value && {children}} + +) + +export const SideBar = () => { + const { nav } = url.params + return ( +
+
+
+ {!isCollapsed.value &&

Project

} + +
+ +
+ + Settings + +
+
+
+ ) +} diff --git a/web/index.tsx b/web/index.tsx index e92a902..7634b3c 100644 --- a/web/index.tsx +++ b/web/index.tsx @@ -4,12 +4,17 @@ import { ProjectsPage } from './pages/ProjectsPage.tsx' import { BackgroundPattern } from './components/BackgroundPattern.tsx' import { Header } from './layout.tsx' import { user } from './lib/session.ts' +import { url } from './lib/router.tsx' +import { ProjectPage } from './pages/ProjectPage.tsx' const renderPage = () => { if (user.pending) return if (!user.data) { return } + if (url.path.startsWith('/projects/')) { + return + } return } const App = () => { diff --git a/web/pages/ProjectPage.tsx b/web/pages/ProjectPage.tsx new file mode 100644 index 0000000..8f45678 --- /dev/null +++ b/web/pages/ProjectPage.tsx @@ -0,0 +1,25 @@ +import { SideBar } from '../components/SideBar.tsx' +import { url } from '../lib/router.tsx' +import { PageContent, PageHeader, PageLayout } from './ProjectsPage.tsx' + +export function ProjectPage() { + const slug = url.path.split('/')[2] + + return ( +
+ +
+ + +

+ Project: {slug} +

+
+ +

This is the project page for {slug}.

+
+
+
+
+ ) +} diff --git a/web/pages/ProjectsPage.tsx b/web/pages/ProjectsPage.tsx index f1b71fb..7e4b05e 100644 --- a/web/pages/ProjectsPage.tsx +++ b/web/pages/ProjectsPage.tsx @@ -166,7 +166,7 @@ async function removeUserFromTeam(user: User, team: Team) { } } -const PageLayout = ( +export const PageLayout = ( { children }: { children: JSX.Element | JSX.Element[] }, ) => (
@@ -175,7 +175,7 @@ const PageLayout = (
) -const PageHeader = ( +export const PageHeader = ( { children }: { children: JSX.Element | JSX.Element[] }, ) => (
@@ -184,7 +184,7 @@ const PageHeader = (
) -const PageContent = ( +export const PageContent = ( { children }: { children: JSX.Element | JSX.Element[] }, ) => (
{children}
From 5f410b58704f5b6123b4e30e311e390b7975e8e7 Mon Sep 17 00:00:00 2001 From: Abdou TOP Date: Tue, 19 Aug 2025 15:40:24 +0000 Subject: [PATCH 2/3] feat(layout): Add PageLayoutWithSideBar and refactor project pages to use it --- web/components/Layout.tsx | 39 +++++++++++++++++++++++++++ web/components/SideBar.tsx | 40 +++++++++++++++------------- web/pages/ProjectPage.tsx | 39 +++++++++++++-------------- web/pages/ProjectsPage.tsx | 26 ++---------------- web/pages/project/DeploymentPage.tsx | 18 +++++++++++++ web/pages/project/SettingsPage.tsx | 18 +++++++++++++ web/pages/project/TaskPage.tsx | 18 +++++++++++++ 7 files changed, 135 insertions(+), 63 deletions(-) create mode 100644 web/components/Layout.tsx create mode 100644 web/pages/project/DeploymentPage.tsx create mode 100644 web/pages/project/SettingsPage.tsx create mode 100644 web/pages/project/TaskPage.tsx diff --git a/web/components/Layout.tsx b/web/components/Layout.tsx new file mode 100644 index 0000000..4045e19 --- /dev/null +++ b/web/components/Layout.tsx @@ -0,0 +1,39 @@ +import { JSX } from 'preact' +import { SideBar } from './SideBar.tsx' + +export const PageLayoutWithSideBar = ( + { children }: { children: JSX.Element | JSX.Element[] | null }, +) => ( +
+ +
+ {children} +
+
+) + +export const PageLayout = ( + { children }: { children: JSX.Element | JSX.Element[] }, +) => ( +
+
+ {children} +
+
+) + +export const PageHeader = ( + { children }: { children: JSX.Element | JSX.Element[] }, +) => ( +
+
+ {children} +
+
+) + +export const PageContent = ( + { children }: { children: JSX.Element | JSX.Element[] }, +) => ( +
{children}
+) diff --git a/web/components/SideBar.tsx b/web/components/SideBar.tsx index cc15071..2f65cf9 100644 --- a/web/components/SideBar.tsx +++ b/web/components/SideBar.tsx @@ -1,15 +1,13 @@ -import { signal } from '@preact/signals' import { ChevronsLeft, ChevronsRight, HardDrive, + ListTodo, LucideIcon, Settings, } from 'lucide-preact' import { A, LinkProps, url } from '../lib/router.tsx' -const isCollapsed = signal(false) - const NavLink = ( { icon: Icon, children, current, ...props }: LinkProps & { current: boolean @@ -22,48 +20,52 @@ const NavLink = ( current ? 'bg-primary/10 text-primary' : 'text-base-content/70 hover:bg-base-300' - } ${isCollapsed.value ? 'justify-center' : ''}`} + } ${url.params.sidebar_collapsed === 'true' ? 'justify-center' : ''}`} > - {!isCollapsed.value && {children}} + {url.params.sidebar_collapsed !== 'true' && {children}} ) export const SideBar = () => { - const { nav } = url.params + const { nav, sidebar_collapsed } = url.params + const isCollapsed = sidebar_collapsed === 'true' return (
-
+
- {!isCollapsed.value &&

Project

} - + {isCollapsed ? : } +
-
+
Settings diff --git a/web/components/forms.tsx b/web/components/forms.tsx new file mode 100644 index 0000000..8d6fd90 --- /dev/null +++ b/web/components/forms.tsx @@ -0,0 +1,128 @@ +import { JSX } from 'preact' +import { useId } from 'preact/hooks' +import { A, LinkProps } from '../lib/router.tsx' + +// Card component +export const Card = ( + { children, title, description }: { + title: string + description?: string + children: JSX.Element | JSX.Element[] + }, +) => ( +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+
+ {children} +
+
+) + +// Input component +export const Input = ( + { label, name, note, ...props }: + & { label: string; name: string; note?: string } + & JSX.InputHTMLAttributes, +) => { + const id = useId() + return ( +
+ + + {note &&

{note}

} +
+ ) +} + +// Button component +export const Button = ( + { children, variant = 'primary', ...props }: + & { + variant?: 'primary' | 'secondary' | 'danger' + } + & Omit, 'class' | 'style'> + & Partial, +) => { + const baseClasses = + 'inline-flex justify-center py-2 px-4 border shadow-sm text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2' + + const variants = { + primary: + 'border-transparent text-white bg-primary hover:bg-primary-hover focus:ring-primary', + secondary: + 'border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-indigo-500', + danger: + 'border-transparent text-white bg-red-600 hover:bg-red-700 focus:ring-red-500', + } + + if (props.href || props.hash || props.params) { + return ( + + {children} + + ) + } + + return ( + + ) +} + +// Switch component +export const Switch = ( + { label, note, ...props }: { + label: string + note?: string + // checked: boolean + } & JSX.InputHTMLAttributes, +) => { + const id = useId() + return ( +
+ + + {note &&

{note}

} +
+
+ + +
+
+ ) +} + +// Note component +export const Note = ({ children }: { children: string }) => ( +

{children}

+) diff --git a/web/pages/ProjectPage.tsx b/web/pages/ProjectPage.tsx index 7fa9b46..ca4e66a 100644 --- a/web/pages/ProjectPage.tsx +++ b/web/pages/ProjectPage.tsx @@ -1,24 +1,91 @@ -import { navigate, url } from '../lib/router.tsx' -import { PageLayoutWithSideBar } from '../components/Layout.tsx' +import { A, navigate, url } from '../lib/router.tsx' +import { + PageContent, + PageHeader, + PageLayoutWithSideBar, +} from '../components/Layout.tsx' import { DeploymentPage } from './project/DeploymentPage.tsx' import { TasksPage } from './project/TaskPage.tsx' import { SettingsPage } from './project/SettingsPage.tsx' +import { api } from '../lib/api.ts' +import { effect } from '@preact/signals' +import { Deployment } from '../../api/schema.ts' const pageMap = { - deployment: , - tasks: , - settings: , + deployment: DeploymentPage, + tasks: TasksPage, + settings: SettingsPage, } +export const deployments: Deployment[] = [ + { + projectId: 'my-awesome-project', + url: 'https://my-app.fly.dev', + logsEnabled: true, + databaseEnabled: false, + sqlEndpoint: undefined, + sqlToken: undefined, + }, + { + projectId: 'my-awesome-project', + url: 'https://staging.my-app.fly.dev', + logsEnabled: false, + databaseEnabled: true, + sqlEndpoint: 'https://db.my-app.com/sql', + sqlToken: 'super-secret-token', + }, +] + +const project = api['GET/api/project'].signal() + +effect(() => { + const path = url.path + const projectSlug = path.split('/')[2] + if (projectSlug) { + project.fetch({ slug: projectSlug }) + } +}) + export function ProjectPage() { const { nav } = url.params - if (!nav || !pageMap[nav as keyof typeof pageMap]) { + + const Component = pageMap[nav as keyof typeof pageMap] + if (!Component) { navigate({ params: { nav: 'deployment' } }) return null } + + if (project.pending) { + return ( + +
Loading...
+
+ ) + } + + if (!project.data) { + return ( + <> + +

+ Deployments +

+
+ +
+

+ Please select a project to view deployments. +

+ Go to Projects +
+
+ + ) + } + return ( - {pageMap[nav as keyof typeof pageMap]} + ) } diff --git a/web/pages/ProjectsPage.tsx b/web/pages/ProjectsPage.tsx index 8a4e3e3..5027c93 100644 --- a/web/pages/ProjectsPage.tsx +++ b/web/pages/ProjectsPage.tsx @@ -56,13 +56,13 @@ async function saveProject( do { finalSlug = base + suffix suffix = suffix ? String(Number(suffix) + 1) : '0' - } while (projects.data?.some((p) => p.projectSlug === finalSlug)) + } while (projects.data?.some((p) => p.slug === finalSlug)) slug = finalSlug } try { await fetcher.fetch({ - projectSlug: finalSlug, - projectName: name, + slug: finalSlug, + name, teamId, repositoryUrl, isPublic: isPublic ?? false, @@ -115,7 +115,7 @@ function toast(message: string, type: 'info' | 'error' = 'info') { async function deleteProject(slug: string) { try { - await api['DELETE/api/project'].fetch({ projectSlug: slug }) + await api['DELETE/api/project'].fetch({ slug: slug }) toast('Project deleted.', 'info') projects.fetch() navigate({ params: { dialog: null, id: null, key: null }, replace: true }) @@ -204,8 +204,8 @@ const ProjectCard = ( const isMember = team.teamMembers.includes(user.data?.userEmail || '') return ( @@ -214,14 +214,14 @@ const ProjectCard = (

- {project.projectName.length > 25 - ? project.projectName.slice(0, 22) + '…' - : project.projectName} + {project.name.length > 25 + ? project.name.slice(0, 22) + '…' + : project.name}

- {project.projectSlug} + {project.slug}
@@ -278,20 +278,20 @@ const TeamMembersRow = ({ user, team }: { user: User; team: Team }) => ( const TeamProjectsRow = ({ project }: { project: ApiProject }) => ( - {project.projectName} - {project.projectSlug} + {project.name} + {project.slug} {project.createdAt && new Date(project.createdAt).toLocaleDateString()} Edit Delete @@ -322,7 +322,7 @@ function ProjectDialog() { const { dialog, slug } = url.params const isEdit = dialog === 'edit-project' const project = isEdit - ? projects.data?.find((p) => p.projectSlug === slug) + ? projects.data?.find((p) => p.slug === slug) : undefined const handleSubmit = (e: Event) => { @@ -353,7 +353,7 @@ function ProjectDialog() { @@ -486,7 +486,7 @@ function TeamProjectsSection({ team }: { team: Team }) { {teamProjects.map((p) => ( - + ))} @@ -612,7 +612,7 @@ function DeleteDialog() { } const name = key === 'project' - ? projects.data?.find((p) => p.projectSlug === id)?.projectName + ? projects.data?.find((p) => p.slug === id)?.name : teams.data?.find((t) => t.teamId === id)?.teamName if (!name) return null @@ -677,8 +677,8 @@ export function ProjectsPage() { const filteredProjects = q ? projects.data?.filter((p) => - p.projectName.toLowerCase().includes(q) || - p.projectSlug.toLowerCase().includes(q) + p.name.toLowerCase().includes(q) || + p.slug.toLowerCase().includes(q) ) : projects.data @@ -752,7 +752,7 @@ export function ProjectsPage() {
{teamProjects.map((p) => ( diff --git a/web/pages/project/DeploymentPage.tsx b/web/pages/project/DeploymentPage.tsx index 765a04b..64c3427 100644 --- a/web/pages/project/DeploymentPage.tsx +++ b/web/pages/project/DeploymentPage.tsx @@ -1,17 +1,179 @@ import { PageContent, PageHeader } from '../../components/Layout.tsx' import { url } from '../../lib/router.tsx' -export const DeploymentPage = () => { - const slug = url.path.split('/')[2] +import { A, navigate } from '../../lib/router.tsx' +import { Calendar, Database, Logs, Search } from 'lucide-preact' +import { Deployment, Project } from '../../../api/schema.ts' +import { deployments } from '../ProjectPage.tsx' + +const DeploymentCard = ({ dep }: { dep: Deployment }) => { + const created = dep.createdAt ? new Date(dep.createdAt) : null + const formattedDate = created ? created.toLocaleString() : 'Unknown' + + return ( + + ) +} + +const LogsSection = () => { + return ( +
+

Logs

+

Log details for the selected deployment will go here.

+
+ ) +} + +const DatabaseSection = () => { + return ( +
+

Database

+

Database details for the selected deployment will go here.

+
+ ) +} + +export const DeploymentPage = ({}: { project: Project }) => { + const { deployment, deptab } = url.params + + const selectedDeployment = deployment + ? deployments.find((d) => d.url === deployment) + : null + + if ( + selectedDeployment && deptab === 'logs' && !selectedDeployment.logsEnabled + ) { + navigate({ + params: { + deptab: selectedDeployment.databaseEnabled ? 'database' : null, + }, + }) + } + + if ( + selectedDeployment && deptab === 'database' && + !selectedDeployment.databaseEnabled + ) { + navigate({ + params: { deptab: selectedDeployment.logsEnabled ? 'logs' : null }, + }) + } + + const tab = deptab === 'logs' ? : return ( <> - -

- Project: {slug} -

+ + +
+
+ +
+ +
+ + +
+
-

This is the project page for {slug}.

+ {selectedDeployment + ? tab + : ( +
+ {deployments.map((dep) => ( + + ))} +
+ )}
) diff --git a/web/pages/project/SettingsPage.tsx b/web/pages/project/SettingsPage.tsx index ece88cd..8cf3d83 100644 --- a/web/pages/project/SettingsPage.tsx +++ b/web/pages/project/SettingsPage.tsx @@ -1,17 +1,292 @@ +import { Deployment, Project, User } from '../../../api/schema.ts' import { PageContent, PageHeader } from '../../components/Layout.tsx' -import { url } from '../../lib/router.tsx' +import { Button, Card, Input, Switch } from '../../components/forms.tsx' +import { useSignal } from '@preact/signals' +import { navigate, url } from '../../lib/router.tsx' +import { JSX } from 'preact' +import { api } from '../../lib/api.ts' +import { deployments } from '../ProjectPage.tsx' +import { user } from '../../lib/session.ts' + +const users = api['GET/api/users'].signal() +users.fetch() + +const team = api['GET/api/team'].signal() + +function ProjectInfoForm({ project }: { project: Project }) { + const handleSubmit = (e: JSX.TargetedEvent) => { + e.preventDefault() + } + + return ( + +
+
+ + +
+
+ +
+
+
+ ) +} + +function DeploymentForm({ deployment }: { deployment?: Deployment }) { + const databaseEnabled = useSignal(deployment?.databaseEnabled || false) + const handleSubmit = (e: JSX.TargetedEvent) => { + e.preventDefault() + } + + return ( + +
+
+ + + databaseEnabled.value = e.currentTarget.checked} + note='Provide an endpoint to execute SQL queries against your database.' + /> + {databaseEnabled.value && ( +
+ + +
+ )} +
+
+ + +
+
+
+ ) +} + +function DeploymentsList({ deployments }: { deployments: Deployment[] }) { + const handleDelete = (_id: string) => { + } + + return ( + +
+ {deployments.map((dep) => ( +
+
+

{dep.url}

+

+ Logs: {dep.logsEnabled ? 'Enabled' : 'Disabled'} | Database: + {' '} + {dep.databaseEnabled ? 'Enabled' : 'Disabled'} +

+
+
+ + +
+
+ ))} +
+
+ +
+
+ ) +} + +function UserManagement() { + const teamMembersDetails = team.data?.teamMembers.map((email) => + users.data?.find((u) => u.userEmail === email) + ).filter(Boolean) as User[] + + const handleAddUser = (e: JSX.TargetedEvent) => { + e.preventDefault() + } + + const handleRemoveUser = (_email: string) => { + } + + return ( + +
+ {teamMembersDetails.map((member) => ( +
+
+ {member.userFullName} +
+

{member.userFullName}

+

+ {member.userEmail} +

+
+
+ +
+ ))} +
+
+

Add a new user

+
+ +
+ +
+
+
+
+ ) +} + +export const SettingsPage = ({ project }: { project: Project }) => { + if (!user.data?.isAdmin) { + navigate({ params: { nav: 'deployments' } }) + } + const { view = 'info', action, id } = url.params + + team.fetch({ teamId: project.teamId }) + + const content = view === 'deployments' + ? ( + action === 'add' + ? + : action === 'edit' && id + ? ( + d.url === id)} + /> + ) + : + ) + : view === 'users' + ? + : -export const SettingsPage = () => { - const slug = url.path.split('/')[2] return ( <> - +

- Project: {slug} + Project Settings: {project.name}

+
+ + + +
-

This is the project page for {slug}.

+
+ {content} +
) diff --git a/web/pages/project/TaskPage.tsx b/web/pages/project/TaskPage.tsx index 33974fc..3017305 100644 --- a/web/pages/project/TaskPage.tsx +++ b/web/pages/project/TaskPage.tsx @@ -1,17 +1,16 @@ +import { Project } from '../../../api/schema.ts' import { PageContent, PageHeader } from '../../components/Layout.tsx' -import { url } from '../../lib/router.tsx' -export const TasksPage = () => { - const slug = url.path.split('/')[2] +export const TasksPage = ({ project }: { project: Project }) => { return ( <>

- Project: {slug} + Project: {project.name}

-

This is the project page for {slug}.

+

This is the project page for {project.name}.

)