feat: scaffold templates page
This commit is contained in:
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Button } from "@headlessui/react";
|
import { Button } from "@headlessui/react";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
TbLayoutSidebarLeftCollapse,
|
TbLayoutSidebarLeftCollapse,
|
||||||
@@ -15,10 +16,11 @@ import membersIconDark from "~/assets/members-dark.json";
|
|||||||
import membersIconLight from "~/assets/members-light.json";
|
import membersIconLight from "~/assets/members-light.json";
|
||||||
import settingsIconDark from "~/assets/settings-dark.json";
|
import settingsIconDark from "~/assets/settings-dark.json";
|
||||||
import settingsIconLight from "~/assets/settings-light.json";
|
import settingsIconLight from "~/assets/settings-light.json";
|
||||||
|
import templatesIconDark from "~/assets/templates-dark.json";
|
||||||
|
import templatesIconLight from "~/assets/templates-light.json";
|
||||||
import ReactiveButton from "~/components/ReactiveButton";
|
import ReactiveButton from "~/components/ReactiveButton";
|
||||||
import UserMenu from "~/components/UserMenu";
|
import UserMenu from "~/components/UserMenu";
|
||||||
import WorkspaceMenu from "~/components/WorkspaceMenu";
|
import WorkspaceMenu from "~/components/WorkspaceMenu";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
|
||||||
interface SideNavigationProps {
|
interface SideNavigationProps {
|
||||||
user: UserType;
|
user: UserType;
|
||||||
@@ -69,6 +71,11 @@ export default function SideNavigation({
|
|||||||
href: "/boards",
|
href: "/boards",
|
||||||
icon: isDarkMode ? boardsIconDark : boardsIconLight,
|
icon: isDarkMode ? boardsIconDark : boardsIconLight,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: t`Templates`,
|
||||||
|
href: "/templates",
|
||||||
|
icon: isDarkMode ? templatesIconDark : templatesIconLight,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: t`Members`,
|
name: t`Members`,
|
||||||
href: "/members",
|
href: "/members",
|
||||||
|
|||||||
17
apps/web/src/pages/templates/index.tsx
Normal file
17
apps/web/src/pages/templates/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
|
import { getDashboardLayout } from "~/components/Dashboard";
|
||||||
|
import Popup from "~/components/Popup";
|
||||||
|
import TemplatesView from "~/views/templates";
|
||||||
|
|
||||||
|
const TemplatesPage: NextPageWithLayout = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TemplatesView />
|
||||||
|
<Popup />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TemplatesPage.getLayout = (page) => getDashboardLayout(page);
|
||||||
|
|
||||||
|
export default TemplatesPage;
|
||||||
40
apps/web/src/views/templates/index.tsx
Normal file
40
apps/web/src/views/templates/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { t } from "@lingui/core/macro";
|
||||||
|
|
||||||
|
import { PageHead } from "~/components/PageHead";
|
||||||
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
export default function TemplatesView() {
|
||||||
|
const { workspace } = useWorkspace();
|
||||||
|
const { data: templates, isLoading } = api.board.templates.useQuery(
|
||||||
|
{ workspacePublicId: workspace.publicId ?? "" },
|
||||||
|
{ enabled: !!workspace.publicId },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-auto h-full max-w-[1100px] p-6 px-5 md:px-28 md:py-12">
|
||||||
|
<PageHead title={t`Templates | ${workspace.name ?? "Workspace"}`} />
|
||||||
|
<div className="relative z-10 mb-8 flex w-full items-center justify-between">
|
||||||
|
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
|
{t`Templates`}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
{isLoading && <div>{t`Loading templates...`}</div>}
|
||||||
|
{!isLoading && (templates?.length ?? 0) === 0 && (
|
||||||
|
<div className="text-sm text-light-900 dark:text-dark-900">{t`No templates yet`}</div>
|
||||||
|
)}
|
||||||
|
{templates?.map((tpl) => (
|
||||||
|
<Link
|
||||||
|
key={tpl.publicId}
|
||||||
|
href={`/boards/${workspace.slug}/${tpl.publicId}`}
|
||||||
|
className="rounded border border-light-300 p-3 text-sm hover:bg-light-100 dark:border-dark-300 dark:hover:bg-dark-100"
|
||||||
|
>
|
||||||
|
<div className="font-medium">{tpl.name}</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,51 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
|||||||
import { assertUserInWorkspace } from "../utils/auth";
|
import { assertUserInWorkspace } from "../utils/auth";
|
||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
|
templates: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/workspaces/{workspacePublicId}/templates",
|
||||||
|
summary: "Get templates",
|
||||||
|
description: "Retrieves all templates for a given workspace",
|
||||||
|
tags: ["Boards"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||||
|
.output(
|
||||||
|
z.custom<
|
||||||
|
Awaited<ReturnType<typeof boardRepo.getTemplatesByWorkspaceId>>
|
||||||
|
>(),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `User not authenticated`,
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.workspacePublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Workspace with public ID ${input.workspacePublicId} not found`,
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertUserInWorkspace(ctx.db, userId, workspace.id);
|
||||||
|
|
||||||
|
const result = boardRepo.getAllByWorkspaceId(ctx.db, workspace.id, {
|
||||||
|
type: "template",
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
@@ -225,7 +270,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.lists.length) {
|
if (input.lists?.length) {
|
||||||
const listInputs = input.lists.map((list, index) => ({
|
const listInputs = input.lists.map((list, index) => ({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
name: list,
|
name: list,
|
||||||
@@ -237,7 +282,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
await listRepo.bulkCreate(ctx.db, listInputs);
|
await listRepo.bulkCreate(ctx.db, listInputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.labels.length) {
|
if (input.labels?.length) {
|
||||||
const labelInputs = input.labels.map((label, index) => ({
|
const labelInputs = input.labels.map((label, index) => ({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
name: label,
|
name: label,
|
||||||
|
|||||||
Reference in New Issue
Block a user