Skip to content

Commit

Permalink
feat: adding empty / error states
Browse files Browse the repository at this point in the history
  • Loading branch information
aacevski committed Feb 17, 2025
1 parent 1635373 commit 5a0ae89
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 18 deletions.
9 changes: 7 additions & 2 deletions apps/api/src/project/controllers/create-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import db from "../../database";
import { projectTable } from "../../database/schema";
import type { CreateProjectPayload } from "../db/queries";

function createProject(
async function createProject(
body: Pick<CreateProjectPayload, "name" | "slug" | "workspaceId" | "icon">,
) {
return db.insert(projectTable).values(body);
const [createdProject] = await db
.insert(projectTable)
.values(body)
.returning();

return createdProject;
}

export default createProject;
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { cn } from "@/lib/cn";
import useProjectStore from "@/store/project";
import { useUserPreferencesStore } from "@/store/user-preferences";
import useWorkspaceStore from "@/store/workspace";
import { useNavigate } from "@tanstack/react-router";
import { Settings } from "lucide-react";
import SignOutButton from "./sign-out-button";

function BottomActions() {
const { isSidebarOpened } = useUserPreferencesStore();
const navigate = useNavigate();
const { setProject } = useProjectStore();
const { setWorkspace } = useWorkspaceStore();

const handleClickSettings = () => {
setProject(undefined);
setWorkspace(undefined);
navigate({ to: "/dashboard/settings/appearance" });
};

return (
<div className={cn("flex gap-1", !isSidebarOpened && "hidden")}>
<button
type="button"
onClick={() => navigate({ to: "/dashboard/settings/appearance" })}
onClick={handleClickSettings}
className="flex-1 px-2 py-1.5 text-xs text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800 rounded-lg flex items-center justify-center transition-colors"
>
<Settings className="w-3 h-3 mr-1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import generateProjectSlug from "@/lib/generate-project-id";
import useWorkspaceStore from "@/store/workspace";
import * as Dialog from "@radix-ui/react-dialog";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "@tanstack/react-router";
import { X } from "lucide-react";
import { useState } from "react";

Expand All @@ -28,13 +29,21 @@ function CreateProjectModal({ open, onClose }: CreateProjectModalProps) {
icon: selectedIcon,
});
const IconComponent = icons[selectedIcon as keyof typeof icons];
const navigate = useNavigate();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;

await mutateAsync();
const { data } = await mutateAsync();
await queryClient.invalidateQueries({ queryKey: ["projects"] });
navigate({
to: "/dashboard/workspace/$workspaceId/project/$projectId/board",
params: {
workspaceId: workspace?.id ?? "",
projectId: data?.id ?? "",
},
});

setName("");
setSlug("");
Expand Down
26 changes: 23 additions & 3 deletions apps/web/src/components/common/sidebar/sections/projects/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ function Projects({ workspaceId }: ProjectsProps) {
</button>
</div>
<div className="space-y-0.5">
{projects &&
projects.length > 0 &&
{projects && projects.length > 0 ? (
projects.map((project) => (
<button
type="button"
Expand All @@ -109,7 +108,28 @@ function Projects({ workspaceId }: ProjectsProps) {
)}
{isSidebarOpened && project.name}
</button>
))}
))
) : isSidebarOpened ? (
<div className="px-3 py-4 flex flex-col items-center text-center">
<div className="w-12 h-12 mb-3 rounded-full bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center">
<Layout className="w-6 h-6 text-indigo-500 dark:text-indigo-400" />
</div>
<p className="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
Create your first project
</p>
<p className="text-xs text-zinc-500 dark:text-zinc-400 mb-3">
Start organizing your work with a new project
</p>
<button
type="button"
onClick={() => setIsCreateProjectOpen(true)}
className="text-xs px-3 py-1.5 bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-500/10 dark:hover:bg-indigo-500/20 text-indigo-600 dark:text-indigo-400 rounded-md transition-colors font-medium inline-flex items-center gap-1.5"
>
<Plus className="w-3 h-3" />
New Project
</button>
</div>
) : null}
</div>
<CreateProjectModal
open={isCreateProjectOpen}
Expand Down
50 changes: 48 additions & 2 deletions apps/web/src/components/kanban-board/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,53 @@ function KanbanBoard() {
setActiveId(null);
};

if (!project || !project?.columns) return <>TODO: Empty state.</>;
if (!project || !project.columns) {
return (
<div className="h-full flex flex-col">
<header className="mb-6 space-y-6 shrink-0 px-6">
<div className="flex items-center justify-between">
<div className="w-48 h-8 bg-zinc-100 dark:bg-zinc-800/50 rounded-md animate-pulse" />
</div>
</header>

<div className="flex-1 relative min-h-0">
<div className="flex gap-6 overflow-x-auto pb-4 px-4 md:px-6 h-full">
{[...Array(3)].map((_, i) => (
<div
key={`kanban-column-skeleton-${
// biome-ignore lint/suspicious/noArrayIndexKey: It's a skeleton
i
}`}
className="flex-1 min-w-80 flex flex-col bg-zinc-50 dark:bg-zinc-900 rounded-lg h-full"
>
<div className="px-4 py-3 flex items-center justify-between">
<div className="w-24 h-5 bg-zinc-100 dark:bg-zinc-800/50 rounded animate-pulse" />
<div className="w-8 h-5 bg-zinc-100 dark:bg-zinc-800/50 rounded animate-pulse" />
</div>

<div className="px-2 pb-4 flex flex-col gap-3 flex-1">
{[...Array(3)].map((_, j) => (
<div
key={`kanban-task-skeleton-${
// biome-ignore lint/suspicious/noArrayIndexKey: It's a skeleton
j
}`}
className="p-4 bg-white dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-800/50 animate-pulse"
>
<div className="space-y-3">
<div className="w-2/3 h-4 bg-zinc-200 dark:bg-zinc-700/50 rounded" />
<div className="w-1/2 h-3 bg-zinc-200 dark:bg-zinc-700/50 rounded" />
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}

const activeTask = activeId
? project.columns
Expand All @@ -95,7 +141,7 @@ function KanbanBoard() {
sensors={sensors}
>
<div className="h-full flex flex-col">
<header className="mb-6 space-y-6 shrink-0 px-4 md:px-0">
<header className="mb-6 space-y-6 shrink-0 px-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100">
{project?.name}
Expand Down
57 changes: 57 additions & 0 deletions apps/web/src/components/project/empty-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { LayoutGrid, Plus } from "lucide-react";
import { useState } from "react";
import CreateProjectModal from "../common/sidebar/sections/projects/create-project-modal";

function EmptyProjectState() {
const [isCreateProjectOpen, setIsCreateProjectOpen] = useState(false);

return (
<div className="flex w-full items-center justify-center h-screen flex-col bg-zinc-50 dark:bg-zinc-950">
<div className="max-w-md w-full px-8">
<div className="text-center mb-8">
<div className="w-14 h-14 mb-4 rounded-full bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center mx-auto">
<LayoutGrid className="w-7 h-7 text-indigo-500 dark:text-indigo-400" />
</div>
<h1 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
Create your first project
</h1>
<p className="text-zinc-500 dark:text-zinc-400 text-center max-w-md mb-6">
Get started by creating a project to organize your tasks and
collaborate with your team.
</p>
</div>

<div className="space-y-4">
<div className="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 rounded-full bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center">
<Plus className="w-4 h-4 text-indigo-500 dark:text-indigo-400" />
</div>
<div className="flex-1">
<h3 className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
New Project
</h3>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Create a project to organize your tasks
</p>
</div>
</div>
<button
type="button"
onClick={() => setIsCreateProjectOpen(true)}
className="w-full px-4 py-2 bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-500/10 dark:hover:bg-indigo-500/20 text-indigo-600 dark:text-indigo-400 rounded-lg transition-colors font-medium inline-flex items-center justify-center gap-2 text-sm"
>
Create Project
</button>
</div>
</div>
</div>
<CreateProjectModal
open={isCreateProjectOpen}
onClose={() => setIsCreateProjectOpen(false)}
/>
</div>
);
}

export default EmptyProjectState;
58 changes: 58 additions & 0 deletions apps/web/src/components/workspace/empty-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { CreateWorkspaceModal } from "../common/sidebar/sections/workspaces/components/create-workspace-modal";

import { LayoutGrid, Plus } from "lucide-react";
import { useState } from "react";

function EmptyWorkspaceState() {
const [isCreateWorkspaceOpen, setIsCreateWorkspaceOpen] = useState(false);

return (
<div className="flex w-full items-center justify-center h-screen flex-col bg-zinc-50 dark:bg-zinc-950">
<div className="max-w-md w-full px-8">
<div className="text-center mb-8">
<div className="w-14 h-14 mb-4 rounded-full bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center mx-auto">
<LayoutGrid className="w-7 h-7 text-indigo-500 dark:text-indigo-400" />
</div>
<h1 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
Create your first workspace
</h1>
<p className="text-zinc-500 dark:text-zinc-400 text-center max-w-md mb-6">
Get started by creating a workspace to organize your projects and
collaborate with your team.
</p>
</div>

<div className="space-y-4">
<div className="p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 rounded-full bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center">
<Plus className="w-4 h-4 text-indigo-500 dark:text-indigo-400" />
</div>
<div className="flex-1">
<h3 className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
New Workspace
</h3>
<p className="text-xs text-zinc-500 dark:text-zinc-400">
Create a workspace for your team
</p>
</div>
</div>
<button
type="button"
onClick={() => setIsCreateWorkspaceOpen(true)}
className="w-full px-4 py-2 bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-500/10 dark:hover:bg-indigo-500/20 text-indigo-600 dark:text-indigo-400 rounded-lg transition-colors font-medium inline-flex items-center justify-center gap-2 text-sm"
>
Create Workspace
</button>
</div>
</div>
</div>
<CreateWorkspaceModal
open={isCreateWorkspaceOpen}
onClose={() => setIsCreateWorkspaceOpen(false)}
/>
</div>
);
}

export default EmptyWorkspaceState;
11 changes: 8 additions & 3 deletions apps/web/src/routes/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Sidebar } from "@/components/common/sidebar";
import { Outlet, redirect } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import EmptyWorkspaceState from "@/components/workspace/empty-state";
import useGetWorkspaces from "@/hooks/queries/workspace/use-get-workspaces";
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";

export const Route = createFileRoute("/dashboard")({
component: DashboardIndexRouteComponent,
Expand All @@ -14,11 +15,15 @@ export const Route = createFileRoute("/dashboard")({
});

function DashboardIndexRouteComponent() {
const { data } = useGetWorkspaces();

const isWorkspacesEmpty = data && data.length === 0;

return (
<>
<Sidebar />
<main className="flex-1 overflow-hidden scroll-smooth">
<Outlet />
{isWorkspacesEmpty ? <EmptyWorkspaceState /> : <Outlet />}
</main>
</>
);
Expand Down
20 changes: 18 additions & 2 deletions apps/web/src/routes/dashboard/workspace/$workspaceId.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import EmptyProjectState from "@/components/project/empty-state";
import useGetProjects from "@/hooks/queries/project/use-get-projects";
import useGetWorkspace from "@/hooks/queries/workspace/use-get-workspace";
import useWorkspaceStore from "@/store/workspace";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { LayoutGrid } from "lucide-react";
import { useEffect } from "react";

export const Route = createFileRoute("/dashboard/workspace/$workspaceId")({
Expand All @@ -10,13 +13,26 @@ export const Route = createFileRoute("/dashboard/workspace/$workspaceId")({
function RouteComponent() {
const { workspaceId } = Route.useParams();
const { setWorkspace } = useWorkspaceStore();
const { data } = useGetWorkspace({ workspaceId });
const { data, isLoading } = useGetWorkspace({ workspaceId });
const { data: projects } = useGetProjects({ workspaceId });

useEffect(() => {
if (data) {
setWorkspace(data);
}
}, [data, setWorkspace]);

return <Outlet />;
if (isLoading) {
return (
<div className="flex w-full items-center justify-center h-screen flex-col md:flex-row bg-zinc-50 dark:bg-zinc-950">
<div className="p-1.5 bg-linear-to-br from-indigo-500 to-purple-500 rounded-lg shadow-xs animate-spin">
<LayoutGrid className="w-5 h-5 text-white" />
</div>
</div>
);
}

const isProjectsEmpty = projects && projects.length === 0;

return <>{isProjectsEmpty ? <EmptyProjectState /> : <Outlet />}</>;
}
Loading

0 comments on commit 5a0ae89

Please sign in to comment.