initial commit

This commit is contained in:
Zacharias-Brohn
2026-01-14 06:12:55 +01:00
commit d702390660
46 changed files with 21386 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store
next-env.d.ts
/app/generated/prisma
+1
View File
@@ -0,0 +1 @@
24.12.0
+2
View File
@@ -0,0 +1,2 @@
.next
next-env.d.ts
+35
View File
@@ -0,0 +1,35 @@
/** @type {import("@ianvs/prettier-plugin-sort-imports").PrettierConfig} */
const config = {
printWidth: 100,
singleQuote: true,
trailingComma: 'es5',
plugins: ['@ianvs/prettier-plugin-sort-imports'],
importOrder: [
'.*styles.css$',
'',
'dayjs',
'^react$',
'^next$',
'^next/.*$',
'<BUILTIN_MODULES>',
'<THIRD_PARTY_MODULES>',
'^@mantine/(.*)$',
'^@mantinex/(.*)$',
'^@mantine-tests/(.*)$',
'^@docs/(.*)$',
'^@/.*$',
'^../(?!.*.css$).*$',
'^./(?!.*.css$).*$',
'\\.css$',
],
overrides: [
{
files: '*.mdx',
options: {
printWidth: 70,
},
},
],
};
export default config;
+16
View File
@@ -0,0 +1,16 @@
import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
core: {
disableWhatsNewNotifications: true,
disableTelemetry: true,
enableCrashReports: false,
},
stories: ['../components/**/*.(stories|story).@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-themes'],
framework: {
name: '@storybook/nextjs',
options: {},
},
};
export default config;
+41
View File
@@ -0,0 +1,41 @@
import '@mantine/core/styles.css';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import { theme } from '../theme';
export const parameters = {
layout: 'fullscreen',
options: {
showPanel: false,
// @ts-expect-error storybook throws build error for (a: any, b: any)
storySort: (a, b) => a.title.localeCompare(b.title, undefined, { numeric: true }),
},
backgrounds: { disable: true },
};
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Mantine color scheme',
defaultValue: 'light',
toolbar: {
icon: 'mirror',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
],
},
},
};
export const decorators = [
(renderStory: any, context: any) => {
const scheme = (context.globals.theme || 'light') as 'light' | 'dark';
return (
<MantineProvider theme={theme} forceColorScheme={scheme}>
<ColorSchemeScript />
{renderStory()}
</MantineProvider>
);
},
];
+2
View File
@@ -0,0 +1,2 @@
.next
out
+28
View File
@@ -0,0 +1,28 @@
{
"extends": ["stylelint-config-standard-scss"],
"rules": {
"custom-property-pattern": null,
"selector-class-pattern": null,
"scss/no-duplicate-mixins": null,
"declaration-empty-line-before": null,
"declaration-block-no-redundant-longhand-properties": null,
"alpha-value-notation": null,
"custom-property-empty-line-before": null,
"property-no-vendor-prefix": null,
"color-function-notation": null,
"length-zero-no-unit": null,
"selector-not-notation": null,
"no-descending-specificity": null,
"comment-empty-line-before": null,
"scss/at-mixin-pattern": null,
"scss/at-rule-no-unknown": null,
"value-keyword-case": null,
"media-feature-range-notation": null,
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global"]
}
]
}
}
+942
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.12.0.cjs
+177
View File
@@ -0,0 +1,177 @@
# Agent Instructions for Next.js + Mantine Project
This document provides context, rules, and workflows for AI agents operating in this codebase.
## 1. Project Overview & Commands
### Core Scripts
The project uses `npm` for dependency management and script execution.
- **Development Server:** `npm run dev`
- Starts the Next.js development server on port 3000.
- **Production Build:** `npm run build`
- Creates an optimized production build.
- **Start Production:** `npm run start`
- Runs the built application in production mode.
- **Type Check:** `npm run typecheck`
- Runs TypeScript compiler (`tsc`) without emitting files to verify types.
- **Storybook:** `npm run storybook`
- Launches the Storybook UI environment for component development.
### Linting & Formatting
Always ensure code passes these checks before submitting changes.
- **Lint All:** `npm run lint` (Runs ESLint and Stylelint)
- **ESLint:** `npm run eslint` (Checks JavaScript/TypeScript rules)
- **Stylelint:** `npm run stylelint` (Checks CSS/SCSS modules)
- **Prettier Check:** `npm run prettier:check` (Verifies formatting)
- **Prettier Fix:** `npm run prettier:write` (Fixes formatting issues automatically)
### Testing
- **Run All Checks:** `npm test`
- Comprehensive check: typegen, prettier, lint, typecheck, and unit tests.
- **Run Unit Tests:** `npm run jest`
- Runs the Jest test suite.
- **Run Single Test File:** `npm run jest -- components/MyComponent/MyComponent.test.tsx`
- **Critical:** Use this when working on a specific component to save time.
- **Watch Mode:** `npm run jest:watch`
## 2. Architecture & File Structure
### Directory Layout
```text
.
├── app/ # Next.js App Router pages and layouts
├── components/ # Shared React components (Atomic design preferred)
│ └── Feature/ # Feature-specific directory
│ ├── Feature.tsx # Main component file
│ ├── Feature.module.css # CSS Modules
│ ├── Feature.story.tsx # Storybook file
│ └── Feature.test.tsx # Jest test file
├── public/ # Static assets (images, fonts, etc.)
├── theme.ts # Mantine theme overrides and configuration
└── ...config files
```
### Framework Conventions
- **Next.js App Router:**
- Use `page.tsx` for routes.
- Use `layout.tsx` for wrapping pages.
- default to **Server Components**.
- Add `'use client';` at the very top of the file only when interactivity (hooks, event listeners) is required.
- **Mantine UI:**
- Use `@mantine/core` components for structure (`Stack`, `Group`, `Grid`) instead of raw `div`s with CSS flexbox.
- Use `rem` functions for sizing to respect user settings.
## 3. Code Style & Standards
### TypeScript
- **Strict Mode:** Enabled. No implicit `any`.
- **Interfaces:** Prefer `interface` over `type` for object definitions.
- **Props:** Define a specific interface for component props, exported if reusable.
```typescript
export interface MyComponentProps {
title: string;
isActive?: boolean;
}
```
### Naming Conventions
- **Components:** `PascalCase` (e.g., `UserProfile.tsx`).
- **Functions/Hooks:** `camelCase` (e.g., `useAuth`, `handleSubmit`).
- **CSS Modules:** `camelCase` for class names.
```css
/* styles.module.css */
.container { ... } /* Good */
.user-card { ... } /* Avoid kebab-case in modules if possible for dot notation access */
```
- **Tests:** `ComponentName.test.tsx`.
### Imports
- **Path Aliases:** Always use `@/` to refer to the project root.
- `import { Button } from '@mantine/core';`
- `import { MyComp } from '@/components/MyComp';`
- **Sorting:** Imports are automatically sorted. Run `npm run prettier:write` if the linter complains.
## 4. Component Development Workflow
When creating or modifying a component (e.g., `UserProfile`), follow this checklist:
1. **Scaffold Files:**
- `UserProfile.tsx`
- `UserProfile.module.css`
- `UserProfile.test.tsx`
- `UserProfile.story.tsx`
2. **Implementation:**
- Define strict Props interface.
- Use Mantine components for layout.
- Use CSS Modules for custom styling not covered by Mantine props.
3. **Theming:**
- Use `theme` object from Mantine for colors/spacing.
- Support light/dark mode using Mantine's mixins or standard CSS variables if needed.
4. **Testing:**
- Write a basic render test.
- Test user interactions (clicks, inputs) using `@testing-library/user-event`.
- Ensure accessibility (`aria-` attributes) if creating custom interactive elements.
5. **Storybook:**
- Create a basic story to visualize the component in isolation.
## 5. Testing & Verification
### Jest & React Testing Library
- **Queries:** Prioritize accessibility-based queries:
1. `getByRole` (buttons, links, headings)
2. `getByLabelText` (form inputs)
3. `getByText` (non-interactive content)
4. `getByTestId` (last resort)
- **Mocking:**
- Mock external modules utilizing `jest.mock`.
- Use `jest.setup.cjs` for global mocks if needed (like `window.matchMedia`).
### Example Test Pattern
```tsx
import { render, screen } from '@/test-utils'; // Use project test-utils if available
import { MyComponent } from './MyComponent';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent title="Test" />);
expect(screen.getByRole('heading', { name: /test/i })).toBeInTheDocument();
});
});
```
## 6. Error Handling & Best Practices
- **Async/Await:** Use `try/catch` blocks for API calls.
- **Validation:** Use `zod` if installed for schema validation, otherwise use strict TypeScript checks.
- **Accessibility:**
- Ensure all `img` tags have `alt` text.
- Ensure buttons have discernible text or `aria-label`.
- Verify contrast ratios using the Storybook accessibility addon if available.
- **Performance:**
- Use `next/image` for images.
- Avoid heavy computations in render cycles; use `useMemo` sparingly and only when proven necessary.
## 7. Troubleshooting
- **Style Issues:** If styles aren't applying, check if `postcss.config.cjs` is correctly processing the file and that the class is applied via `className={classes.myClass}`.
- **Hydration Errors:** Ensure HTML structure is valid (no `div` inside `p`) and that the server/client output matches. Use `useEffect` for browser-only rendering if needed.
- **Test Failures:** If `jest` fails on imports, check `jest.config.cjs` for `moduleNameMapper` settings matching `tsconfig.json` paths.
## 8. Documentation & External Resources
- **Mantine Documentation:**
- **CRITICAL:** When implementing Mantine components or features, you MUST refer to the official AI-optimized documentation.
- **URL:** [https://mantine.dev/llms.txt](https://mantine.dev/llms.txt)
- Use the `WebFetch` tool to retrieve the latest patterns and examples from this URL if you are unsure about the implementation details or best practices for the current version.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Vitaly Rtischev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+37
View File
@@ -0,0 +1,37 @@
# Mantine Next.js template
This is a template for [Next.js](https://nextjs.org/) app router + [Mantine](https://mantine.dev/).
If you want to use pages router instead, see [next-pages-template](https://github.com/mantinedev/next-pages-template).
## Features
This template comes with the following features:
- [PostCSS](https://postcss.org/) with [mantine-postcss-preset](https://mantine.dev/styles/postcss-preset)
- [TypeScript](https://www.typescriptlang.org/)
- [Storybook](https://storybook.js.org/)
- [Jest](https://jestjs.io/) setup with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
- ESLint setup with [eslint-config-mantine](https://github.com/mantinedev/eslint-config-mantine)
## npm scripts
### Build and dev scripts
- `dev` start dev server
- `build` bundle application for production
- `analyze` analyzes application bundle with [@next/bundle-analyzer](https://www.npmjs.com/package/@next/bundle-analyzer)
### Testing scripts
- `typecheck` checks TypeScript types
- `lint` runs ESLint
- `prettier:check` checks files with Prettier
- `jest` runs jest tests
- `jest:watch` starts jest watch
- `test` runs `jest`, `prettier:check`, `lint` and `typecheck` scripts
### Other scripts
- `storybook` starts storybook dev server
- `storybook:build` build production storybook bundle to `storybook-static`
- `prettier:write` formats all files with Prettier
+53
View File
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { SignJWT } from 'jose';
import { prisma } from '@/lib/prisma';
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key-at-least-32-chars-long'
);
export async function POST(request: NextRequest) {
try {
const { username, password } = await request.json();
if (!username || !password) {
return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
}
const user = await prisma.user.findUnique({
where: { username },
});
if (!user) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
const token = await new SignJWT({ userId: user.id, username: user.username })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('30d')
.sign(JWT_SECRET);
const response = NextResponse.json({ message: 'Login successful' }, { status: 200 });
response.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60, // 30 days
path: '/',
});
return response;
} catch (error) {
console.error('Login error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+15
View File
@@ -0,0 +1,15 @@
import { NextResponse } from 'next/server';
export async function POST() {
const response = NextResponse.json({ message: 'Logged out successfully' }, { status: 200 });
response.cookies.set('token', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 0, // Expire immediately
path: '/',
});
return response;
}
+21
View File
@@ -0,0 +1,21 @@
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key-at-least-32-chars-long'
);
export async function GET(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.json({ user: null }, { status: 200 });
}
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
return NextResponse.json({ user: payload }, { status: 200 });
} catch (error) {
return NextResponse.json({ user: null }, { status: 200 });
}
}
+64
View File
@@ -0,0 +1,64 @@
import { NextRequest, NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { prisma } from '@/lib/prisma';
export async function POST(request: NextRequest) {
try {
const { username, password } = await request.json();
if (!username || !password) {
return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
}
const existingUser = await prisma.user.findUnique({
where: { username },
});
if (existingUser) {
return NextResponse.json({ error: 'Username already exists' }, { status: 400 });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
username,
password: hashedPassword,
},
});
// Automatically login after register
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key-at-least-32-chars-long'
);
// Import SignJWT dynamically to avoid top-level import if it causes issues, though it should be fine.
// Better yet, let's keep it consistent with login.
const { SignJWT } = await import('jose');
const token = await new SignJWT({ userId: user.id, username: user.username })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('30d')
.sign(JWT_SECRET);
const response = NextResponse.json(
{ message: 'User created successfully', userId: user.id },
{ status: 201 }
);
// Set cookie on response
await response.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60, // 30 days
path: '/',
});
return response;
} catch (error: any) {
console.error('Registration error:', error);
return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 });
}
}
+101
View File
@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';
import { prisma } from '@/lib/prisma';
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key-at-least-32-chars-long'
);
export async function GET(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
const userId = payload.userId as string;
const chats = await prisma.chat.findMany({
where: { userId },
orderBy: { updatedAt: 'desc' },
include: {
messages: {
orderBy: { createdAt: 'asc' },
},
},
});
return NextResponse.json(chats, { status: 200 });
} catch (error) {
console.error('Fetch chats error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
const userId = payload.userId as string;
const { messages, chatId } = await request.json();
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return NextResponse.json({ error: 'Messages are required' }, { status: 400 });
}
// Determine chat ID or create new
let currentChatId = chatId;
// Use the last message in the array, assuming it's the one to be saved
const messageToSave = messages[messages.length - 1];
if (!currentChatId) {
// Create new chat
const firstMessageContent = messageToSave.content;
const title =
firstMessageContent.length > 30
? `${firstMessageContent.substring(0, 30)}...`
: firstMessageContent;
const newChat = await prisma.chat.create({
data: {
userId,
title,
},
});
currentChatId = newChat.id;
}
const { content, role } = messageToSave;
if (!content || !role) {
return NextResponse.json({ error: 'Invalid message format' }, { status: 400 });
}
const message = await prisma.message.create({
data: {
content,
role,
chatId: currentChatId,
},
});
// Update chat updated_at
await prisma.chat.update({
where: { id: currentChatId },
data: { updatedAt: new Date() },
});
return NextResponse.json({ message, chatId: currentChatId }, { status: 200 });
} catch (error) {
console.error('Save chat error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
+28
View File
@@ -0,0 +1,28 @@
import '@mantine/core/styles.css';
import React from 'react';
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
import { DynamicThemeProvider } from '@/components/DynamicThemeProvider';
export const metadata = {
title: 'Mantine Next.js template',
description: 'I am using Mantine with Next.js!',
};
export default function RootLayout({ children }: { children: any }) {
return (
<html lang="en" {...mantineHtmlProps}>
<head>
<ColorSchemeScript />
<link rel="shortcut icon" href="/favicon.svg" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
/>
</head>
<body>
<DynamicThemeProvider>{children}</DynamicThemeProvider>
</body>
</html>
);
}
+5
View File
@@ -0,0 +1,5 @@
import ChatLayout from '@/components/Chat/ChatLayout';
export default function HomePage() {
return <ChatLayout />;
}
+12
View File
@@ -0,0 +1,12 @@
.chatBubble {
max-width: 80%;
line-height: 1.6;
}
.chatBubbleUser {
border-top-right-radius: 0;
}
.chatBubbleAssistant {
border-top-left-radius: 0;
}
+10
View File
@@ -0,0 +1,10 @@
import { render, screen } from '@/test-utils';
import ChatLayout from './ChatLayout';
describe('ChatLayout', () => {
it('renders chat interface', () => {
render(<ChatLayout />);
expect(screen.getByText('AI Chat')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Type your message...')).toBeInTheDocument();
});
});
+370
View File
@@ -0,0 +1,370 @@
'use client';
import { useEffect, useState } from 'react';
import {
IconLayoutSidebar,
IconMessage,
IconPlus,
IconRobot,
IconSend,
IconSettings,
IconUser,
} from '@tabler/icons-react';
import {
ActionIcon,
AppShell,
Avatar,
Burger,
Container,
Group,
Paper,
rem,
ScrollArea,
Stack,
Text,
TextInput,
Title,
Tooltip,
UnstyledButton,
useMantineTheme,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useThemeContext } from '@/components/DynamicThemeProvider';
import { SettingsModal } from '@/components/Settings/SettingsModal';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
}
interface Chat {
id: string;
title: string;
updatedAt: string;
messages?: Message[];
}
export default function ChatLayout() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const [settingsOpened, { open: openSettings, close: closeSettings }] = useDisclosure(false);
const { primaryColor, setPrimaryColor } = useThemeContext();
const theme = useMantineTheme();
// State
const [chats, setChats] = useState<Chat[]>([]);
const [activeChatId, setActiveChatId] = useState<string | null>(null);
const [messages, setMessages] = useState<Message[]>([
{
id: '1',
role: 'assistant',
content: 'Hello! I am an AI assistant. How can I help you today?',
},
]);
const [inputValue, setInputValue] = useState('');
const [isInputFocused, setIsInputFocused] = useState(false);
const [isLoadingChats, setIsLoadingChats] = useState(false);
// Fetch chats on load
useEffect(() => {
fetchChats();
}, [settingsOpened]); // Refresh when settings close (might have logged in/out)
const fetchChats = async () => {
setIsLoadingChats(true);
try {
const res = await fetch('/api/chats');
if (res.ok) {
const data = await res.json();
if (Array.isArray(data)) {
setChats(data);
} else {
setChats([]);
}
} else {
setChats([]);
}
} catch (e) {
console.error('Failed to fetch chats', e);
setChats([]);
} finally {
setIsLoadingChats(false);
}
};
const handleSelectChat = (chat: Chat) => {
setActiveChatId(chat.id);
if (chat.messages) {
setMessages(chat.messages);
} else {
// In a real app we might fetch full messages here if not included in list
setMessages([]);
}
if (mobileOpened) {
toggleMobile();
}
};
const handleNewChat = () => {
setActiveChatId(null);
setMessages([
{
id: Date.now().toString(),
role: 'assistant',
content: 'Hello! I am an AI assistant. How can I help you today?',
},
]);
if (mobileOpened) {
toggleMobile();
}
};
const handleSendMessage = async () => {
if (!inputValue.trim()) {
return;
}
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
};
// Optimistic update
const newMessages = [...messages, userMessage];
setMessages(newMessages);
setInputValue('');
try {
// Save to backend
const res = await fetch('/api/chats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [userMessage],
chatId: activeChatId,
}),
});
if (res.ok) {
const data = await res.json();
if (data.chatId && data.chatId !== activeChatId) {
setActiveChatId(data.chatId);
fetchChats(); // Refresh list to show new chat
}
// Simulate AI response
setTimeout(async () => {
const responseMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content:
'I am a simulated AI response. I do not have a backend yet. I just repeat that I am simulated.',
};
const updatedMessages = [...newMessages, responseMessage];
setMessages(updatedMessages);
// Save AI response to backend
try {
await fetch('/api/chats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [responseMessage],
chatId: data.chatId || activeChatId,
}),
});
} catch (e) {
console.error(e);
}
}, 1000);
}
} catch (e) {
console.error('Failed to save message', e);
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleSendMessage();
}
};
return (
<>
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="md"
>
<AppShell.Header>
<Group h="100%" px="md" justify="space-between">
<Group>
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Tooltip label="Toggle Sidebar">
<ActionIcon variant="subtle" color="gray" onClick={toggleDesktop} visibleFrom="sm">
<IconLayoutSidebar size={20} />
</ActionIcon>
</Tooltip>
<IconRobot size={28} stroke={1.5} color={theme.colors[primaryColor][6]} />
<Title order={3}>AI Chat</Title>
</Group>
<ActionIcon variant="subtle" color="gray" onClick={openSettings}>
<IconSettings size={20} />
</ActionIcon>
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
style={{ borderRight: '1px solid var(--mantine-color-default-border)' }}
>
<Stack gap="sm" h="100%">
<Group justify="space-between">
<Title order={5} c="dimmed">
History
</Title>
<Tooltip label="New Chat">
<ActionIcon variant="light" color={primaryColor} onClick={handleNewChat}>
<IconPlus size={18} />
</ActionIcon>
</Tooltip>
</Group>
<ScrollArea style={{ flex: 1, margin: '0 -10px' }} p="xs">
<Stack gap="xs">
{chats.length > 0 ? (
chats.map((chat) => (
<UnstyledButton
key={chat.id}
onClick={() => handleSelectChat(chat)}
p="sm"
style={{
borderRadius: 'var(--mantine-radius-md)',
backgroundColor:
activeChatId === chat.id
? 'var(--mantine-color-default-hover)'
: 'transparent',
transition: 'background-color 0.2s',
}}
>
<Group wrap="nowrap">
<IconMessage size={18} color="gray" style={{ minWidth: 18 }} />
<Text size="sm" truncate>
{chat.title}
</Text>
</Group>
</UnstyledButton>
))
) : (
<Text size="sm" c="dimmed" ta="center" mt="xl">
{isLoadingChats ? 'Loading...' : 'No saved chats'}
</Text>
)}
</Stack>
</ScrollArea>
</Stack>
</AppShell.Navbar>
<AppShell.Main>
<Container
size="lg"
h="calc(100vh - 100px)"
style={{ display: 'flex', flexDirection: 'column' }}
>
<ScrollArea flex={1} mb="md" type="auto" offsetScrollbars>
<Stack gap="xl" px="md" py="lg">
{messages.map((message) => (
<Group
key={message.id}
justify={message.role === 'user' ? 'flex-end' : 'flex-start'}
align="flex-start"
wrap="nowrap"
>
{message.role === 'assistant' && (
<Avatar radius="xl" color={primaryColor} variant="light">
<IconRobot size={20} />
</Avatar>
)}
<Paper
p="md"
radius="lg"
bg={
message.role === 'user'
? 'var(--mantine-color-default-hover)'
: 'transparent'
}
style={{
maxWidth: '80%',
borderTopLeftRadius: message.role === 'assistant' ? 0 : undefined,
borderTopRightRadius: message.role === 'user' ? 0 : undefined,
}}
>
<Text size="sm" style={{ lineHeight: 1.6 }}>
{message.content}
</Text>
</Paper>
{message.role === 'user' && (
<Avatar radius="xl" color="gray" variant="light">
<IconUser size={20} />
</Avatar>
)}
</Group>
))}
</Stack>
</ScrollArea>
<Paper
withBorder
p="xs"
radius="xl"
shadow="sm"
style={{
transition: 'border-color 0.2s ease',
borderColor: isInputFocused ? theme.colors[primaryColor][6] : undefined,
}}
>
<Group gap="xs">
<TextInput
variant="unstyled"
placeholder="Type your message..."
value={inputValue}
onChange={(event) => setInputValue(event.currentTarget.value)}
onKeyDown={handleKeyDown}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
style={{ flex: 1, paddingLeft: rem(10) }}
size="md"
/>
<ActionIcon
onClick={handleSendMessage}
variant="filled"
color={primaryColor}
size="lg"
radius="xl"
disabled={!inputValue.trim()}
>
<IconSend size={18} />
</ActionIcon>
</Group>
</Paper>
</Container>
</AppShell.Main>
</AppShell>
<SettingsModal
opened={settingsOpened}
close={closeSettings}
primaryColor={primaryColor}
setPrimaryColor={setPrimaryColor}
/>
</>
);
}
+32
View File
@@ -0,0 +1,32 @@
'use client';
import { createContext, useContext, useState } from 'react';
import { createTheme, MantineProvider } from '@mantine/core';
interface ThemeContextType {
primaryColor: string;
setPrimaryColor: (color: string) => void;
}
const ThemeContext = createContext<ThemeContextType>({
primaryColor: 'blue',
setPrimaryColor: () => {},
});
export const useThemeContext = () => useContext(ThemeContext);
export function DynamicThemeProvider({ children }: { children: React.ReactNode }) {
const [primaryColor, setPrimaryColor] = useState('blue');
const theme = createTheme({
primaryColor,
});
return (
<ThemeContext.Provider value={{ primaryColor, setPrimaryColor }}>
<MantineProvider theme={theme} defaultColorScheme="auto">
{children}
</MantineProvider>
</ThemeContext.Provider>
);
}
+249
View File
@@ -0,0 +1,249 @@
import { useEffect, useState } from 'react';
import { IconAlertCircle, IconPalette, IconUser, IconX } from '@tabler/icons-react';
import {
ActionIcon,
Alert,
Button,
ColorSwatch,
Divider,
Group,
Modal,
NavLink,
PasswordInput,
rem,
Stack,
Text,
TextInput,
Title,
useMantineTheme,
} from '@mantine/core';
interface User {
id: string;
username: string;
}
interface SettingsModalProps {
opened: boolean;
close: () => void;
primaryColor: string;
setPrimaryColor: (color: string) => void;
}
export function SettingsModal({
opened,
close,
primaryColor,
setPrimaryColor,
}: SettingsModalProps) {
const theme = useMantineTheme();
const [activeTab, setActiveTab] = useState<'appearance' | 'account'>('appearance');
// Account State
const [user, setUser] = useState<User | null>(null);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoginMode, setIsLoginMode] = useState(true);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
// Check login status on mount
useEffect(() => {
if (opened) {
fetchUser();
}
}, [opened]);
const fetchUser = async () => {
try {
const res = await fetch('/api/auth/me');
const data = await res.json();
if (data.user) {
setUser(data.user);
} else {
setUser(null);
}
} catch (e) {
console.error(e);
}
};
const handleAuth = async () => {
setError('');
setLoading(true);
const endpoint = isLoginMode ? '/api/auth/login' : '/api/auth/register';
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Something went wrong');
}
// Refresh user state
await fetchUser();
setUsername('');
setPassword('');
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleLogout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
setUser(null);
};
const colors = Object.keys(theme.colors).filter(
(color) => color !== 'dark' && color !== 'gray' && color !== 'white' && color !== 'black'
);
return (
<Modal
opened={opened}
onClose={close}
withCloseButton={false}
size="lg"
padding={0}
radius="xl"
>
<Group align="stretch" gap={0} style={{ minHeight: 400, overflow: 'hidden' }}>
{/* Left Sidebar */}
<Stack
gap="xs"
w={220}
p="sm"
bg="var(--mantine-color-default-hover)"
style={{
borderRight: '1px solid var(--mantine-color-default-border)',
}}
>
<NavLink
active={activeTab === 'appearance'}
label="Appearance"
leftSection={<IconPalette size={18} stroke={1.5} />}
variant="light"
color={primaryColor}
onClick={() => setActiveTab('appearance')}
style={{ borderRadius: 'var(--mantine-radius-lg)' }}
/>
<NavLink
active={activeTab === 'account'}
label="Account"
leftSection={<IconUser size={18} stroke={1.5} />}
variant="light"
color={primaryColor}
onClick={() => setActiveTab('account')}
style={{ borderRadius: 'var(--mantine-radius-lg)' }}
/>
</Stack>
{/* Right Content */}
<Stack p="xl" style={{ flex: 1, position: 'relative' }}>
<ActionIcon
onClick={close}
variant="subtle"
color="gray"
size="sm"
style={{ position: 'absolute', top: rem(15), right: rem(15), zIndex: 1 }}
>
<IconX size={20} />
</ActionIcon>
{activeTab === 'appearance' && (
<>
<Title order={4}>Appearance</Title>
<Text size="sm" c="dimmed">
Customize the look and feel of the application.
</Text>
<Divider my="sm" />
<Stack gap="xs">
<Text size="sm" fw={500}>
Accent Color
</Text>
<Group gap="xs">
{colors.map((color) => (
<ColorSwatch
key={color}
component="button"
color={theme.colors[color][6]}
onClick={() => setPrimaryColor(color)}
style={{ color: '#fff', cursor: 'pointer' }}
withShadow
>
{primaryColor === color && <IconPalette size={12} />}
</ColorSwatch>
))}
</Group>
</Stack>
</>
)}
{activeTab === 'account' && (
<>
<Title order={4}>Account</Title>
<Text size="sm" c="dimmed">
Manage your account and chat history.
</Text>
<Divider my="sm" />
{user ? (
<Stack>
<Text>
Logged in as <b>{user.username}</b>
</Text>
<Button color="red" variant="light" onClick={handleLogout}>
Log out
</Button>
</Stack>
) : (
<Stack>
{error && (
<Alert icon={<IconAlertCircle size={16} />} title="Error" color="red">
{error}
</Alert>
)}
<TextInput
label="Username"
placeholder="Enter username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<PasswordInput
label="Password"
placeholder="Enter password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Group justify="space-between" mt="md">
<Text
size="xs"
style={{ cursor: 'pointer' }}
c="blue"
onClick={() => setIsLoginMode(!isLoginMode)}
>
{isLoginMode ? 'Need an account? Register' : 'Have an account? Login'}
</Text>
<Button onClick={handleAuth} loading={loading} color={primaryColor}>
{isLoginMode ? 'Login' : 'Register'}
</Button>
</Group>
</Stack>
)}
</>
)}
</Stack>
</Group>
</Modal>
);
}
BIN
View File
Binary file not shown.
+22
View File
@@ -0,0 +1,22 @@
import mantine from 'eslint-config-mantine';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
// @ts-check
export default defineConfig(
tseslint.configs.recommended,
...mantine,
{ ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}', '.next'] },
{
files: ['**/*.story.tsx'],
rules: { 'no-console': 'off' },
},
{
languageOptions: {
parserOptions: {
tsconfigRootDir: process.cwd(),
project: ['./tsconfig.json'],
},
},
}
);
+16
View File
@@ -0,0 +1,16 @@
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/components/$1',
'^@/pages/(.*)$': '<rootDir>/pages/$1',
},
testEnvironment: 'jest-environment-jsdom',
};
module.exports = createJestConfig(customJestConfig);
+27
View File
@@ -0,0 +1,27 @@
require('@testing-library/jest-dom');
const { getComputedStyle } = window;
window.getComputedStyle = (elt) => getComputedStyle(elt);
window.HTMLElement.prototype.scrollIntoView = () => {};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserver;
+13
View File
@@ -0,0 +1,13 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ['query'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
+12
View File
@@ -0,0 +1,12 @@
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default withBundleAnalyzer({
reactStrictMode: false,
experimental: {
optimizePackageImports: ['@mantine/core', '@mantine/hooks'],
},
});
+18636
View File
File diff suppressed because it is too large Load Diff
+72
View File
@@ -0,0 +1,72 @@
{
"name": "mantine-next-template",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"analyze": "ANALYZE=true next build",
"start": "next start",
"typecheck": "tsc --noEmit",
"lint": "npm run eslint && npm run stylelint",
"eslint": "eslint .",
"stylelint": "stylelint '**/*.css' --cache",
"jest": "jest",
"jest:watch": "jest --watch",
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
"test": "npx next typegen && npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build"
},
"dependencies": {
"@mantine/core": "^8.3.12",
"@mantine/hooks": "^8.3.12",
"@next/bundle-analyzer": "^16.0.0",
"@prisma/client": "^5.10.2",
"@tabler/icons-react": "^3.35.0",
"bcryptjs": "^3.0.3",
"jose": "^6.1.3",
"next": "16.1.1",
"prisma": "^5.10.2",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@babel/core": "^7.28.4",
"@eslint/eslintrc": "^3",
"@eslint/js": "^9.37.0",
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@storybook/addon-themes": "^10.0.0",
"@storybook/nextjs": "^10.0.0",
"@storybook/react": "^10.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/bcryptjs": "^2.4.6",
"@types/eslint-plugin-jsx-a11y": "^6",
"@types/jest": "^30.0.0",
"@types/node": "^25.0.0",
"@types/react": "19.2.8",
"babel-loader": "^10.0.0",
"eslint": "^9.37.0",
"eslint-config-mantine": "^4.0.3",
"eslint-config-next": "16.1.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"postcss": "^8.5.6",
"postcss-preset-mantine": "1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.6.2",
"storybook": "^10.0.0",
"stylelint": "^16.25.0",
"stylelint-config-standard-scss": "^16.0.0",
"ts-jest": "^29.4.4",
"typescript": "5.9.3",
"typescript-eslint": "^8.46.0"
},
"packageManager": "yarn@4.12.0"
}
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
+15
View File
@@ -0,0 +1,15 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import 'dotenv/config';
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: 'file:./dev.db',
},
});
BIN
View File
Binary file not shown.
@@ -0,0 +1,29 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"password" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "Chat" (
"id" TEXT NOT NULL PRIMARY KEY,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"title" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "Chat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Message" (
"id" TEXT NOT NULL PRIMARY KEY,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"role" TEXT NOT NULL,
"content" TEXT NOT NULL,
"chatId" TEXT NOT NULL,
CONSTRAINT "Message_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"
+37
View File
@@ -0,0 +1,37 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
username String @unique
password String
chats Chat[]
}
model Chat {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
userId String
user User @relation(fields: [userId], references: [id])
messages Message[]
}
model Message {
id String @id @default(cuid())
createdAt DateTime @default(now())
role String // 'user' or 'assistant'
content String
chatId String
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><g fill="none" fill-rule="evenodd"><rect width="500" height="500" fill="#339AF0" rx="250"/><g fill="#FFF"><path fill-rule="nonzero" d="M202.055 135.706c-6.26 8.373-4.494 20.208 3.944 26.42 29.122 21.45 45.824 54.253 45.824 90.005 0 35.752-16.702 68.559-45.824 90.005-8.436 6.215-10.206 18.043-3.944 26.42 6.26 8.378 18.173 10.13 26.611 3.916a153.835 153.835 0 0024.509-22.54h53.93c10.506 0 19.023-8.455 19.023-18.885 0-10.43-8.517-18.886-19.023-18.886h-29.79c8.196-18.594 12.553-38.923 12.553-60.03s-4.357-41.436-12.552-60.03h29.79c10.505 0 19.022-8.455 19.022-18.885 0-10.43-8.517-18.886-19.023-18.886h-53.93a153.835 153.835 0 00-24.509-22.54c-8.438-6.215-20.351-4.46-26.61 3.916z"/><path d="M171.992 246.492c0-15.572 12.624-28.195 28.196-28.195 15.572 0 28.195 12.623 28.195 28.195 0 15.572-12.623 28.196-28.195 28.196-15.572 0-28.196-12.624-28.196-28.196z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 937 B

+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"schedule": ["before 5am on sunday"],
"groupName": "all dependencies",
"packageRules": [
{
"matchPackagePatterns": ["*"],
"groupName": "all dependencies"
}
],
"prHourlyLimit": 0,
"prConcurrentLimit": 0
}
+15
View File
@@ -0,0 +1,15 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
try {
const users = await prisma.user.findMany();
console.log('Users:', users);
} catch (e) {
console.error('Error:', e);
} finally {
await prisma.$disconnect();
}
}
main();
+5
View File
@@ -0,0 +1,5 @@
import userEvent from '@testing-library/user-event';
export * from '@testing-library/react';
export { render } from './render';
export { userEvent };
+13
View File
@@ -0,0 +1,13 @@
import { render as testingLibraryRender } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import { theme } from '../theme';
export function render(ui: React.ReactNode) {
return testingLibraryRender(<>{ui}</>, {
wrapper: ({ children }: { children: React.ReactNode }) => (
<MantineProvider theme={theme} env="test">
{children}
</MantineProvider>
),
});
}
+7
View File
@@ -0,0 +1,7 @@
'use client';
import { createTheme } from '@mantine/core';
export const theme = createTheme({
/* Put your mantine theme override here */
});
+37
View File
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"types": ["node", "jest", "@testing-library/jest-dom"],
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
".storybook/main.ts",
".storybook/preview.tsx",
"next-env.d.ts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}