This is a starter project using Next.js, Typescript, NPM, Shadcn/UI, Prettier, and the Prettier Tailwind plugin.
- 📝 Type Safety: Always write types where needed for your components and API calls.
- 🧩 Separation of Concerns: Keep your code modular and organized by separating different concerns.
- 🔧 Utility Functions: Create reusable utility functions to avoid code duplication.
- 🎨 Styling: Use TailwindCSS for consistent and efficient styling.
- 🖌️ UI Components: Utilize Shadcn/UI for building user interface components.
-
Clone the repository:
git clone https://github.com/logiconllc/nextjs-starter-template cd nextjs-starter-template -
Install dependencies using NPM:
npm install
To start the development server:
npm run devTo build the project for production:
npm run buildTo start the production server:
npm run startThis project uses react-icons for icons. You can easily include any icon from popular icon libraries.
To use an icon in your component:
import { FaBeer } from "react-icons/fa";
const MyComponent = () => {
return <FaBeer />;
};To handle forms, this project uses react-hook-form. For form validations, zod is used.
Install the required libraries:
npm install react-hook-form zodHere's an example of how to create a form with validation:
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
});
export function ProfileForm() {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
},
});
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
export default ProfileForm;This project uses @tanstack/react-table for creating and managing tables.
Install the required library:
npm install @tanstack/react-tableHere's an example of how to create a table:
We are going to build a table to show recent payments. Here's what our data looks like:
type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const payments: Payment[] = [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "m@example.com",
},
{
id: "489e1d42",
amount: 125,
status: "processing",
email: "example@gmail.com",
},
// ...
];Start by creating the following file structure:
app
└── payments
├── columns.tsx
├── data-table.tsx
└── page.tsxI'm using a Next.js example here but this works for any other React framework.
columns.tsx(client component) will contain our column definitions.data-table.tsx(client component) will contain our<DataTable />component.page.tsx(server component) is where we'll fetch data and render our table.
Let's start by building a basic table.
First, we'll define our columns.
"use client";
import { ColumnDef } from "@tanstack/react-table";
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
export const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "amount",
header: "Amount",
},
];Note: Columns are where you define the core of what your table will look like. They define the data that will be displayed, how it will be formatted, sorted and filtered.
Next, we'll create a <DataTable /> component to render our table.
"use client";
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}Tip: If you find yourself using <DataTable /> in multiple places, this is the component you could make reusable by extracting it to components/ui/data-table.tsx.
<DataTable columns={columns} data={data} />
Finally, we'll render our table in our page component.
import { Payment, columns } from "./columns";
import { DataTable } from "./data-table";
async function getData(): Promise<Payment[]> {
// Fetch data from your API here.
return [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "m@example.com",
},
// ...
];
}
export default async function DemoPage() {
const data = await getData();
return (
<div className="container mx-auto py-10">
<DataTable columns={columns} data={data} />
</div>
);
}This project uses Prettier for code formatting. The Prettier Tailwind plugin is also included to ensure Tailwind classes are ordered correctly.
To format your code, run:
npm run prettierShadcn/UI is used for the UI components.