Enhance template with improved UI and gitignore handling

This commit is contained in:
Aman Varshney
2025-03-22 01:52:38 +05:30
parent 4b59bca82e
commit fc59f630e0
13 changed files with 297 additions and 50 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Enhance template with improved UI

View File

@@ -24,9 +24,17 @@ export async function createProject(options: ProjectConfig): Promise<string> {
} }
await fs.copy(templateDir, projectDir); await fs.copy(templateDir, projectDir);
const gitignorePath = path.join(projectDir, "_gitignore"); const gitignorePaths = [
path.join(projectDir, "_gitignore"),
path.join(projectDir, "packages/client/_gitignore"),
path.join(projectDir, "packages/server/_gitignore"),
];
for (const gitignorePath of gitignorePaths) {
if (await fs.pathExists(gitignorePath)) { if (await fs.pathExists(gitignorePath)) {
await fs.move(gitignorePath, path.join(projectDir, ".gitignore")); const targetPath = path.join(path.dirname(gitignorePath), ".gitignore");
await fs.move(gitignorePath, targetPath);
}
} }
if (options.auth) { if (options.auth) {

View File

@@ -1,16 +1,97 @@
import { trpc } from "@/utils/trpc"; import { trpc } from "@/utils/trpc";
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: HomeComponent, component: HomeComponent,
}); });
const TITLE_TEXT = `
██████╗ ███████╗████████╗████████╗███████╗██████╗
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████╗ ██║ ███████║██║ █████╔╝
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
`;
function HomeComponent() { function HomeComponent() {
const healthCheck = trpc.healthCheck.useQuery(); const healthCheck = trpc.healthCheck.useQuery();
return ( return (
<div className="p-2"> <div className="container mx-auto max-w-3xl px-4 py-2">
<h3>Welcome Home!</h3> <pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
<p>healthCheck: {healthCheck.data}</p> <div className="grid gap-6">
<section className="rounded-lg border p-4">
<h2 className="mb-2 font-medium">API Status</h2>
<div className="flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
></div>
<span className="text-sm text-muted-foreground">
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected"
: "Disconnected"}
</span>
</div>
</section>
<section>
<h2 className="mb-3 font-medium">Core Features</h2>
<ul className="grid grid-cols-2 gap-3">
<FeatureItem
title="Type-Safe API"
description="End-to-end type safety with tRPC"
/>
<FeatureItem
title="Modern React"
description="TanStack Router + TanStack Query"
/>
<FeatureItem
title="Fast Backend"
description="Lightweight Hono server"
/>
<FeatureItem
title="Beautiful UI"
description="TailwindCSS + shadcn/ui components"
/>
</ul>
</section>
<div className="mt-4 flex flex-col gap-4 sm:flex-row sm:items-center">
<Button asChild>
<Link to="/todos" className="flex items-center">
View Todo Demo
<ArrowRight className="ml-1 h-4 w-4" />
</Link>
</Button>
</div>
</div>
</div> </div>
); );
} }
function FeatureItem({
title,
description,
}: {
title: string;
description: string;
}) {
return (
<li className="border-l-2 border-primary py-1 pl-3">
<h3 className="font-medium">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
</li>
);
}

View File

@@ -8,62 +8,35 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { Skeleton } from "./ui/skeleton"; import { Skeleton } from "./ui/skeleton";
import { Link } from "@tanstack/react-router";
export default function UserMenu() { export default function UserMenu() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: session, isPending } = authClient.useSession(); const { data: session, isPending } = authClient.useSession();
useEffect(() => {
if (!session && !isPending) {
navigate({
to: "/",
});
}
}, [session, isPending]);
if (isPending) { if (isPending) {
return <Skeleton className="h-9 w-24" />; return <Skeleton className="h-9 w-24" />;
} }
if (!session) { if (!session) {
return ( return (
<DropdownMenu> <Button variant="outline" asChild>
<DropdownMenuTrigger asChild> <Link to="/login">Sign In</Link>
<Button variant="outline">Sign In</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Button
variant="outline"
className="w-full"
onClick={() => {
navigate({
to: "/",
});
}}
>
Sign In
</Button> </Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
); );
} }
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline">{session?.user.name}</Button> <Button variant="outline">{session.user.name}</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="bg-card"> <DropdownMenuContent className="bg-card">
<DropdownMenuLabel>My Account</DropdownMenuLabel> <DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem>{session?.user.email}</DropdownMenuItem> <DropdownMenuItem>{session.user.email}</DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Button <Button
variant="destructive" variant="destructive"

View File

@@ -5,11 +5,6 @@ import { useEffect } from "react";
export const Route = createFileRoute("/dashboard")({ export const Route = createFileRoute("/dashboard")({
component: RouteComponent, component: RouteComponent,
loader: async ({ context: { trpcQueryUtils } }) => {
await trpcQueryUtils.healthCheck.ensureData();
await trpcQueryUtils.privateData.ensureData();
return;
},
}); });
function RouteComponent() { function RouteComponent() {

View File

@@ -1,19 +1,97 @@
import AuthForms from "@/components/auth-forms";
import { trpc } from "@/utils/trpc"; import { trpc } from "@/utils/trpc";
import { createFileRoute, Link } from "@tanstack/react-router"; import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: HomeComponent, component: HomeComponent,
}); });
const TITLE_TEXT = `
██████╗ ███████╗████████╗████████╗███████╗██████╗
██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████╗ ██║ ███████║██║ █████╔╝
██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
`;
function HomeComponent() { function HomeComponent() {
const healthCheck = trpc.healthCheck.useQuery(); const healthCheck = trpc.healthCheck.useQuery();
return ( return (
<div className="p-2"> <div className="container mx-auto max-w-3xl px-4 py-2">
<h3>Welcome Home!</h3> <pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
<Link to="/dashboard">Go to Dashboard</Link> <div className="grid gap-6">
<p>healthCheck: {healthCheck.data}</p> <section className="rounded-lg border p-4">
<AuthForms /> <h2 className="mb-2 font-medium">API Status</h2>
<div className="flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
></div>
<span className="text-sm text-muted-foreground">
{healthCheck.isLoading
? "Checking..."
: healthCheck.data
? "Connected"
: "Disconnected"}
</span>
</div>
</section>
<section>
<h2 className="mb-3 font-medium">Core Features</h2>
<ul className="grid grid-cols-2 gap-3">
<FeatureItem
title="Type-Safe API"
description="End-to-end type safety with tRPC"
/>
<FeatureItem
title="Modern React"
description="TanStack Router + TanStack Query"
/>
<FeatureItem
title="Fast Backend"
description="Lightweight Hono server"
/>
<FeatureItem
title="Beautiful UI"
description="TailwindCSS + shadcn/ui components"
/>
</ul>
</section>
<div className="mt-4 flex flex-col gap-4 sm:flex-row sm:items-center">
<Button asChild>
<Link to="/todos" className="flex items-center">
View Todo Demo
<ArrowRight className="ml-1 h-4 w-4" />
</Link>
</Button>
</div>
</div>
</div> </div>
); );
} }
function FeatureItem({
title,
description,
}: {
title: string;
description: string;
}) {
return (
<li className="border-l-2 border-primary py-1 pl-3">
<h3 className="font-medium">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
</li>
);
}

View File

@@ -0,0 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import AuthForms from "@/components/auth-forms";
export const Route = createFileRoute("/login")({
component: RouteComponent,
});
function RouteComponent() {
return (
<AuthForms />);
}

View File

@@ -0,0 +1,24 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -0,0 +1,24 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -0,0 +1,24 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});

View File

@@ -0,0 +1,24 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Authentication required",
cause: "No session",
});
}
return next({
ctx: {
...ctx,
session: ctx.session,
},
});
});