Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions scripts/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ try {
unlinkSync(zipPath);
}

// Create zip archive
// Create zip archive, skipping local-only artifacts that must never ship.
const excluded =
/(^|[/\\])(node_modules|dist)([/\\]|$)|(^|[/\\])\.git([/\\]|$)|(^|[/\\])\.env$/;
const zip = new AdmZip();
zip.addLocalFolder(templatesPath);
zip.addLocalFolder(templatesPath, '', (entry) => !excluded.test(entry));
zip.writeZip(zipPath);

if (!existsSync(zipPath)) {
Expand Down
2 changes: 1 addition & 1 deletion templates/monorepo/apps/backend
Submodule backend updated 108 files
2 changes: 1 addition & 1 deletion templates/monorepo/apps/frontend/biome.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"root": false,
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
"extends": ["../../biome.json"],
"files": {
"includes": [
Expand Down
49 changes: 26 additions & 23 deletions templates/monorepo/apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,38 @@
"routes:watch": "tsr watch"
},
"dependencies": {
"@elysiajs/eden": "^1.4.6",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.9.0",
"@tanstack/react-router": "^1.144.0",
"@tanstack/react-router-devtools": "^1.144.0",
"@tanstack/router-plugin": "^1.145.2",
"@elysiajs/eden": "^1.4.9",
"@tailwindcss/vite": "^4.3.1",
"@tanstack/react-devtools": "^0.10.7",
"@tanstack/react-query": "^5.101.0",
"@tanstack/react-router": "^1.170.16",
"@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-plugin": "^1.168.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"elysia": "^1.4.28",
"elysia-boilerplate": "workspace:*",
"lucide-react": "^0.562.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.6",
"lucide-react": "^1.21.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.1",
"tw-animate-css": "^1.3.6"
},
"devDependencies": {
"@biomejs/biome": "2.3.10",
"@tanstack/devtools-vite": "^0.4.0",
"@tanstack/router-cli": "^1.145.2",
"@biomejs/biome": "2.5.0",
"@tanstack/devtools-vite": "^0.8.0",
"@tanstack/router-cli": "^1.167.17",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^25.0.3",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^4.0.16"
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^26.0.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^6.0.2",
"jsdom": "^29.1.1",
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.9"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
21 changes: 21 additions & 0 deletions templates/monorepo/apps/frontend/src/features/users/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { infiniteQueryOptions } from '@tanstack/react-query';
import { api } from '@/shared/api/client';

const PAGE_SIZE = 10;

async function fetchUsers(cursor?: string) {
const { data, error } = await api.users.get({
query: cursor ? { limit: PAGE_SIZE, cursor } : { limit: PAGE_SIZE },
});
if (error) {
throw new Error(error.value?.message ?? 'Failed to load users');
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return data;
}

export const usersQueryOptions = infiniteQueryOptions({
queryKey: ['users'],
queryFn: ({ pageParam }: { pageParam?: string }) => fetchUsers(pageParam),
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
15 changes: 14 additions & 1 deletion templates/monorepo/apps/frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRouter, RouterProvider } from '@tanstack/react-router';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
Expand All @@ -7,6 +8,16 @@ import { routeTree } from './routeTree.gen';

import './styles.css';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000,
retry: 1,
refetchOnWindowFocus: false,
},
},
});

// Create a new router instance
const router = createRouter({
routeTree,
Expand All @@ -30,7 +41,9 @@ if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<RouterProvider router={router} />
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>,
);
}
6 changes: 3 additions & 3 deletions templates/monorepo/apps/frontend/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const UsersNewRoute = UsersNewRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/users/new': typeof UsersNewRoute
'/users': typeof UsersIndexRoute
'/users/': typeof UsersIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
Expand All @@ -47,7 +47,7 @@ export interface FileRoutesById {
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/users/new' | '/users'
fullPaths: '/' | '/users/new' | '/users/'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/users/new' | '/users'
id: '__root__' | '/' | '/users/new' | '/users/'
Expand All @@ -71,7 +71,7 @@ declare module '@tanstack/react-router' {
'/users/': {
id: '/users/'
path: '/users'
fullPath: '/users'
fullPath: '/users/'
preLoaderRoute: typeof UsersIndexRouteImport
parentRoute: typeof rootRouteImport
}
Expand Down
57 changes: 39 additions & 18 deletions templates/monorepo/apps/frontend/src/routes/users/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { createFileRoute, Link } from '@tanstack/react-router';
import { UserPlus } from 'lucide-react';
import { api } from '@/shared/api/client';
import { usersQueryOptions } from '@/features/users/queries';

export const Route = createFileRoute('/users/')({
component: Users,
loader: async () => {
const { data, error } = await api.users.get({
query: {
limit: 100,
offset: 0,
},
});

if (error) {
throw new Error(error.value as string);
}

return { users: data?.users ?? [], total: data?.total ?? 0 };
},
});

function Users() {
const { users, total } = Route.useLoaderData();
const {
data,
error,
isPending,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useInfiniteQuery(usersQueryOptions);

const users = data?.pages.flatMap((page) => page.users) ?? [];

return (
<div className="min-h-screen bg-gray-50 py-8 px-4 sm:px-6 lg:px-8">
Expand All @@ -38,7 +34,13 @@ function Users() {
</div>

<div className="bg-white rounded-lg shadow-md overflow-hidden">
{users.length === 0 ? (
{isPending ? (
<div className="p-8 text-center text-gray-500">
Loading users...
</div>
) : error && !data ? (
<div className="p-8 text-center text-red-600">{error.message}</div>
) : users.length === 0 ? (
<div className="p-8 text-center text-gray-500">
<p className="text-lg mb-2">No users found</p>
<p className="text-sm">Create your first user to get started.</p>
Expand All @@ -47,7 +49,7 @@ function Users() {
<>
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
<p className="text-sm text-gray-600">
Showing {users.length} of {total} users
Showing {users.length} items
</p>
</div>
<div className="divide-y divide-gray-200">
Expand All @@ -70,6 +72,25 @@ function Users() {
</div>
))}
</div>
{(hasNextPage || error) && (
<div className="px-6 py-4 border-t border-gray-200 text-center">
{hasNextPage && (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-cyan-700 bg-cyan-50 rounded-md hover:bg-cyan-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
)}
{error && (
<p className="mt-2 text-sm text-red-600" role="alert">
{error.message}
</p>
)}
</div>
)}
</>
)}
</div>
Expand Down
15 changes: 12 additions & 3 deletions templates/monorepo/apps/frontend/src/routes/users/new.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useQueryClient } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useState } from 'react';
import { usersQueryOptions } from '@/features/users/queries';
import { api } from '@/shared/api/client';

export const Route = createFileRoute('/users/new')({
Expand All @@ -8,6 +10,7 @@ export const Route = createFileRoute('/users/new')({

function NewUser() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [formData, setFormData] = useState({
name: '',
surname: '',
Expand All @@ -16,7 +19,7 @@ function NewUser() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
Expand All @@ -25,11 +28,14 @@ function NewUser() {
const { data, error: apiError } = await api.users.post(formData);

if (apiError) {
setError(apiError.value);
setError(apiError.value?.message ?? 'Failed to create user');
return;
}

if (data) {
await queryClient.invalidateQueries({
queryKey: usersQueryOptions.queryKey,
});
navigate({ to: '/users' });
}
} catch (err) {
Expand All @@ -55,7 +61,10 @@ function NewUser() {
</h1>

{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
<div
role="alert"
className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md"
>
<p className="text-sm text-red-800">{error}</p>
</div>
)}
Expand Down
Loading