seperated backend and frontend

This commit is contained in:
Berkay 2025-11-14 19:10:12 +03:00
parent 45f6b7a1ef
commit 42983eab65
150 changed files with 26903 additions and 9078 deletions

View File

@ -1,8 +0,0 @@
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { typeDefs, resolvers } from '@/graphql';
const server = new ApolloServer({ typeDefs, resolvers, introspection: process.env.NODE_ENV === 'development' });
const handler = startServerAndCreateNextHandler(server as any, { context: async (req) => ({ req }) });
export { handler as GET, handler as POST };

56
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,56 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

4
backend/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

17
backend/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM node:20-alpine
WORKDIR /usr/src/app
# Copy package files
COPY package*.json ./
RUN npm install
# Copy everything from root
COPY . .
RUN npm run build
EXPOSE 4000
CMD ["node", "dist/server.js"]

98
backend/README.md Normal file
View File

@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

35
backend/eslint.config.mjs Normal file
View File

@ -0,0 +1,35 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
);

8
backend/nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

11463
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

81
backend/package.json Normal file
View File

@ -0,0 +1,81 @@
{
"name": "backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@apollo/server": "^5.1.0",
"@as-integrations/express5": "^1.1.2",
"@nestjs/apollo": "^13.2.1",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/graphql": "^13.2.0",
"@nestjs/mongoose": "^11.0.3",
"@nestjs/platform-express": "^11.0.1",
"graphql": "^16.12.0",
"graphql-fields": "^2.0.3",
"mongoose": "^8.19.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/graphql": "^14.2.3",
"@types/graphql-fields": "^1.3.9",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
getHello(): string {
return this.appService.getHello();
}
}

33
backend/src/app.module.ts Normal file
View File

@ -0,0 +1,33 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersModule } from './users/users.module';
import { PeopleModule } from './people/people.module';
import { BuildService } from './build/build.service';
import { BuildModule } from './build/build.module';
import { BuildPartsModule } from './build-parts/build-parts.module';
import { BuildAreaModule } from './build-area/build-area.module';
import { UserTypesModule } from './user-types/user-types.module';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
playground: true,
}),
MongooseModule.forRoot(process.env.MONGODB_URI || "mongodb://evyosdbuser:evyosdbpassword@10.10.2.13:27017/evyosdb"),
UsersModule,
PeopleModule,
BuildModule,
BuildPartsModule,
BuildAreaModule,
UserTypesModule,
],
controllers: [AppController],
providers: [AppService, BuildService],
})
export class AppModule { }

View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { BuildAreaResolver } from './build-area.resolver';
import { BuildAreaService } from './build-area.service';
import { MongooseModule } from '@nestjs/mongoose';
import { BuildArea, BuildAreaSchema } from '@/models/build-area.model';
@Module({
imports: [MongooseModule.forFeature([{ name: BuildArea.name, schema: BuildAreaSchema }])],
providers: [BuildAreaResolver, BuildAreaService]
})
export class BuildAreaModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BuildAreaResolver } from './build-area.resolver';
describe('BuildAreaResolver', () => {
let resolver: BuildAreaResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BuildAreaResolver],
}).compile();
resolver = module.get<BuildAreaResolver>(BuildAreaResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,28 @@
import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql';
import { Types } from 'mongoose';
import { BuildArea } from '@/models/build-area.model';
import { CreateBuildAreaInput } from './dto/create-build-area.input';
import graphqlFields from 'graphql-fields';
import { BuildAreaService } from './build-area.service';
import type { GraphQLResolveInfo } from 'graphql';
@Resolver()
export class BuildAreaResolver {
constructor(private readonly buildAreaService: BuildAreaService) { }
@Query(() => [BuildArea], { name: 'BuildAreas' })
async getBuildAreas(@Info() info: GraphQLResolveInfo): Promise<BuildArea[]> {
const fields = graphqlFields(info); const projection = this.buildAreaService.buildProjection(fields); return this.buildAreaService.findAll(projection);
}
@Query(() => BuildArea, { name: 'BuildArea', nullable: true })
async getBuildArea(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise<BuildArea | null> {
const fields = graphqlFields(info); const projection = this.buildAreaService.buildProjection(fields); return this.buildAreaService.findById(new Types.ObjectId(id), projection);
}
@Mutation(() => BuildArea, { name: 'createBuildArea' })
async createBuildArea(@Args('input') input: CreateBuildAreaInput): Promise<BuildArea> {
return this.buildAreaService.create(input);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BuildAreaService } from './build-area.service';
describe('BuildAreaService', () => {
let service: BuildAreaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BuildAreaService],
}).compile();
service = module.get<BuildAreaService>(BuildAreaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Types, Model } from 'mongoose';
import { BuildArea, BuildAreaDocument } from '@/models/build-area.model';
import { CreateBuildAreaInput } from './dto/create-build-area.input';
@Injectable()
export class BuildAreaService {
constructor(@InjectModel(BuildArea.name) private readonly buildAreaModel: Model<BuildAreaDocument>) { }
async findAll(projection?: any): Promise<BuildAreaDocument[]> {
return this.buildAreaModel.find({}, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec();
}
async findById(id: Types.ObjectId, projection?: any): Promise<BuildAreaDocument | null> {
return this.buildAreaModel.findById(id, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec();
}
async create(input: CreateBuildAreaInput): Promise<BuildAreaDocument> {
const buildArea = new this.buildAreaModel(input);
return buildArea.save();
}
buildProjection(fields: Record<string, any>): any {
const projection: any = {};
for (const key in fields) {
if (key === 'buildArea' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildArea.${subField}`] = 1 } }
else { projection[key] = 1 }
}
return projection;
}
}

View File

@ -0,0 +1,7 @@
import { InputType } from "@nestjs/graphql";
@InputType()
export class CreateBuildAreaInput {
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { BuildPartsService } from './build-parts.service';
import { BuildPartsResolver } from './build-parts.resolver';
import { MongooseModule } from '@nestjs/mongoose';
import { BuildParts, BuildPartsSchema } from '@/models/build-parts.model';
@Module({
imports: [MongooseModule.forFeature([{ name: BuildParts.name, schema: BuildPartsSchema }])],
providers: [BuildPartsService, BuildPartsResolver]
})
export class BuildPartsModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BuildPartsResolver } from './build-parts.resolver';
describe('BuildPartsResolver', () => {
let resolver: BuildPartsResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BuildPartsResolver],
}).compile();
resolver = module.get<BuildPartsResolver>(BuildPartsResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,28 @@
import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql';
import { Types } from 'mongoose';
import { BuildParts } from '@/models/build-parts.model';
import { CreateBuildPartsInput } from './dto/create-build-part.input';
import graphqlFields from 'graphql-fields';
import type { GraphQLResolveInfo } from 'graphql';
import { BuildPartsService } from './build-parts.service';
@Resolver()
export class BuildPartsResolver {
constructor(private readonly buildPartsService: BuildPartsService) { }
@Query(() => [BuildParts], { name: 'BuildParts' })
async getBuildParts(@Info() info: GraphQLResolveInfo): Promise<BuildParts[]> {
const fields = graphqlFields(info); const projection = this.buildPartsService.buildProjection(fields); return this.buildPartsService.findAll(projection);
}
@Query(() => BuildParts, { name: 'BuildParts', nullable: true })
async getBuildPart(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise<BuildParts | null> {
const fields = graphqlFields(info); const projection = this.buildPartsService.buildProjection(fields); return this.buildPartsService.findById(new Types.ObjectId(id), projection);
}
@Mutation(() => BuildParts, { name: 'createBuildPart' })
async createBuildPart(@Args('input') input: CreateBuildPartsInput): Promise<BuildParts> {
return this.buildPartsService.create(input);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BuildPartsService } from './build-parts.service';
describe('BuildPartsService', () => {
let service: BuildPartsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BuildPartsService],
}).compile();
service = module.get<BuildPartsService>(BuildPartsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Types, Model } from 'mongoose';
import { BuildParts, BuildPartsDocument } from '@/models/build-parts.model';
import { CreateBuildPartsInput } from './dto/create-build-part.input';
@Injectable()
export class BuildPartsService {
constructor(@InjectModel(BuildParts.name) private readonly buildPartsModel: Model<BuildPartsDocument>) { }
async findAll(projection?: any): Promise<BuildPartsDocument[]> {
return this.buildPartsModel.find({}, projection, { lean: false }).populate({ path: 'buildParts', select: projection?.buildParts }).exec();
}
async findById(id: Types.ObjectId, projection?: any): Promise<BuildPartsDocument | null> {
return this.buildPartsModel.findById(id, projection, { lean: false }).populate({ path: 'buildParts', select: projection?.buildParts }).exec();
}
async create(input: CreateBuildPartsInput): Promise<BuildPartsDocument> {
const buildParts = new this.buildPartsModel(input);
return buildParts.save();
}
buildProjection(fields: Record<string, any>): any {
const projection: any = {};
for (const key in fields) {
if (key === 'buildParts' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildParts.${subField}`] = 1 } }
else { projection[key] = 1 }
}
return projection;
}
}

View File

@ -0,0 +1,7 @@
import { InputType, Field, ID } from '@nestjs/graphql';
@InputType()
export class CreateBuildPartsInput {
}

View File

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { BuildResolver } from './build.resolver';
@Module({
providers: [BuildResolver]
})
export class BuildModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BuildResolver } from './build.resolver';
describe('BuildResolver', () => {
let resolver: BuildResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BuildResolver],
}).compile();
resolver = module.get<BuildResolver>(BuildResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,27 @@
import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql';
import { Types } from 'mongoose';
import { Build } from '@/models/build.model';
import { CreateBuildInput } from './dto/create-build.input';
import graphqlFields from 'graphql-fields';
import { BuildService } from './build.service';
import type { GraphQLResolveInfo } from 'graphql';
@Resolver()
export class BuildResolver {
constructor(private readonly buildService: BuildService) { }
@Query(() => [Build], { name: 'Builds' })
async getBuilds(@Info() info: GraphQLResolveInfo): Promise<Build[]> {
const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findAll(projection);
}
@Query(() => Build, { name: 'Build', nullable: true })
async getBuild(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise<Build | null> {
const fields = graphqlFields(info); const projection = this.buildService.buildProjection(fields); return this.buildService.findById(new Types.ObjectId(id), projection);
}
@Mutation(() => Build, { name: 'createBuild' })
async createBuild(@Args('input') input: CreateBuildInput): Promise<Build> {
return this.buildService.create(input);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BuildService } from './build.service';
describe('BuildService', () => {
let service: BuildService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BuildService],
}).compile();
service = module.get<BuildService>(BuildService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Types, Model } from 'mongoose';
import { Build, BuildDocument } from '@/models/build.model';
import { CreateBuildInput } from './dto/create-build.input';
@Injectable()
export class BuildService {
constructor(@InjectModel(Build.name) private readonly buildModel: Model<BuildDocument>) { }
async findAll(projection?: any): Promise<BuildDocument[]> {
return this.buildModel.find({}, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec();
}
async findById(id: Types.ObjectId, projection?: any): Promise<BuildDocument | null> {
return this.buildModel.findById(id, projection, { lean: false }).populate({ path: 'buildArea', select: projection?.buildArea }).exec();
}
async create(input: CreateBuildInput): Promise<BuildDocument> {
const buildArea = new this.buildModel(input);
return buildArea.save();
}
buildProjection(fields: Record<string, any>): any {
const projection: any = {};
for (const key in fields) {
if (key === 'buildArea' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`buildArea.${subField}`] = 1 } }
else { projection[key] = 1 }
}
return projection;
}
}

View File

@ -0,0 +1,8 @@
import { InputType } from "@nestjs/graphql";
@InputType()
export class CreateBuildInput {
}

8
backend/src/main.ts Normal file
View File

@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3001);
}
bootstrap();

View File

@ -0,0 +1,49 @@
import { Prop } from '@nestjs/mongoose';
import { randomUUID } from 'crypto';
export class Base {
@Prop({ default: randomUUID, unique: true })
uuid: string;
@Prop({ default: () => new Date(Date.now()) })
expiryStarts: Date;
@Prop({ default: () => new Date('2099-12-31') })
expiryEnds: Date;
@Prop({ default: false })
isConfirmed: boolean;
@Prop({ default: false })
deleted: boolean;
@Prop({ default: true })
active: boolean;
@Prop({ default: randomUUID })
crypUuId: string;
@Prop({ default: randomUUID })
createdCredentialsToken: string;
@Prop({ default: randomUUID })
updatedCredentialsToken: string;
@Prop({ default: randomUUID })
confirmedCredentialsToken: string;
@Prop({ default: false })
isNotificationSend: boolean;
@Prop({ default: false })
isEmailSend: boolean;
@Prop({ default: 0 })
refInt: number;
@Prop({ default: randomUUID })
refId: string;
@Prop({ default: 0 })
replicationId: number;
}

View File

@ -0,0 +1,47 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { ObjectType, Field, ID, Float } from '@nestjs/graphql';
import { Base } from '@/models/base.model';
@ObjectType()
@Schema({ timestamps: true })
export class BuildAddress extends Base {
@Field()
@Prop({ required: true })
buildNumber: string;
@Field()
@Prop({ required: true })
doorNumber: string;
@Field()
@Prop({ required: true })
floorNumber: string;
@Field()
@Prop({ required: true })
commentAddress: string;
@Field()
@Prop({ required: true })
letterAddress: string;
@Field()
@Prop({ required: true })
shortLetterAddress: string;
@Field(() => Float)
@Prop({ required: true })
latitude: number;
@Field(() => Float)
@Prop({ required: true })
longitude: number;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: 'Street', required: true })
street: Types.ObjectId;
}
export type BuildAddressDocument = BuildAddress & Document;
export const BuildAddressSchema = SchemaFactory.createForClass(BuildAddress);

View File

@ -0,0 +1,36 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { ObjectType, Field, ID, Float } from '@nestjs/graphql';
import { Base } from '@/models/base.model';
@ObjectType()
@Schema({ timestamps: true })
export class BuildArea extends Base {
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: 'Build', required: true })
build: Types.ObjectId;
@Field(() => Float)
@Prop({ required: true })
area: number;
@Field(() => Float)
@Prop({ required: true })
size: number;
@Field()
@Prop({ required: true })
type: string;
@Field()
@Prop({ required: true })
typeToken: string;
@Field()
@Prop({ required: true })
description: string;
}
export type BuildAreaDocument = BuildArea & Document;
export const BuildAreaSchema = SchemaFactory.createForClass(BuildArea);

View File

@ -0,0 +1,60 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { ObjectType, Field, ID, Float } from '@nestjs/graphql';
import { Base } from '@/models/base.model';
@ObjectType()
@Schema({ timestamps: true })
export class BuildParts extends Base {
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: 'Build', required: true })
buildId: Types.ObjectId;
@Field()
@Prop({ required: true })
addressGovCode: string;
@Field(() => Float)
@Prop({ required: true })
no: number;
@Field(() => Float)
@Prop({ required: true })
level: number;
@Field()
@Prop({ required: true })
code: string;
@Field(() => Float)
@Prop({ required: true })
grossSize: number;
@Field(() => Float)
@Prop({ required: true })
netSize: number;
@Field()
@Prop({ required: true })
defaultAccessory: string;
@Field()
@Prop({ required: true })
humanLivability: boolean;
@Field()
@Prop({ required: true })
key: string;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: 'ApiEnumDropdown', required: true })
directionId: Types.ObjectId;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: 'ApiEnumDropdown', required: true })
typeId: Types.ObjectId;
}
export type BuildPartsDocument = BuildParts & Document;
export const BuildPartsSchema = SchemaFactory.createForClass(BuildParts);

View File

@ -0,0 +1,146 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { ObjectType, Field, ID, Int } from '@nestjs/graphql';
import { Base } from '@/models/base.model';
import { Person } from '@/models/person.model';
import { Company } from '@/models/company.model';
@ObjectType()
export class BuildIban {
@Field()
@Prop({ required: true })
iban: string;
@Field()
@Prop({ required: true })
startDate: Date;
@Field()
@Prop({ required: true, default: new Date('2900-01-01T03:00:00+03:00') })
stopDate: Date;
@Field()
@Prop({ required: true, default: 'TR0000000000000' })
bankCode: string;
@Field()
@Prop({ required: true, default: '????' })
xcomment: string;
}
@ObjectType()
export class BuildResponsible {
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: Company.name, required: true })
company: Types.ObjectId;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: Person.name, required: true })
person: Types.ObjectId;
}
@ObjectType()
export class BuildInfo {
@Field()
@Prop({ required: true })
govAddressCode: string;
@Field()
@Prop({ required: true })
buildName: string;
@Field()
@Prop({ required: true })
buildNo: string;
@Field(() => Int)
@Prop({ required: true })
maxFloor: number;
@Field(() => Int)
@Prop({ required: true })
undergroundFloor: number;
@Field()
@Prop({ required: true })
buildDate: Date;
@Field()
@Prop({ required: true })
decisionPeriodDate: Date;
@Field()
@Prop({ required: true })
taxNo: string;
@Field(() => Int)
@Prop({ required: true })
liftCount: number;
@Field()
@Prop({ required: true })
heatingSystem: boolean;
@Field()
@Prop({ required: true })
coolingSystem: boolean;
@Field()
@Prop({ required: true })
hotWaterSystem: boolean;
@Field(() => Int)
@Prop({ required: true })
blockServiceManCount: number;
@Field(() => Int)
@Prop({ required: true })
securityServiceManCount: number;
@Field(() => Int)
@Prop({ required: true })
garageCount: number;
@Field(() => Int)
@Prop({ required: true })
managementRoomId: number;
}
@ObjectType()
@Schema({ timestamps: true })
export class Build extends Base {
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: 'BuildType', required: true })
buildType: Types.ObjectId;
@Field()
@Prop({ required: true, unique: true })
collectionToken: string;
@Field(() => BuildInfo)
@Prop({ type: BuildInfo, required: true })
info: BuildInfo;
@Field(() => ID, { nullable: true })
@Prop({ type: Types.ObjectId, ref: 'Site' })
site?: Types.ObjectId;
@Field(() => ID, { nullable: true })
@Prop({ type: Types.ObjectId, ref: 'BuildAddress' })
address?: Types.ObjectId;
@Field(() => [ID], { nullable: true })
@Prop({ type: [{ type: Types.ObjectId, ref: 'BuildArea' }] })
areas?: Types.ObjectId[];
@Field(() => [BuildIban], { nullable: true })
@Prop({ type: [BuildIban] })
ibans?: BuildIban[];
@Field(() => [BuildResponsible], { nullable: true })
@Prop({ type: [BuildResponsible] })
responsibles?: BuildResponsible[];
}
export type BuildDocument = Build & Document;
export const BuildSchema = SchemaFactory.createForClass(Build);

View File

@ -0,0 +1,16 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
import { Base } from '@/models/base.model';
@ObjectType()
@Schema({ timestamps: true })
export class Company extends Base {
@Field(() => ID)
@Prop({ required: true })
name: string;
}
export type CompanyDocument = Company & Document;
export const CompanySchema = SchemaFactory.createForClass(Company);

View File

@ -0,0 +1,41 @@
import { Document, Model, Types, Connection, connection as defaultConnection } from 'mongoose';
import { randomUUID } from 'crypto';
import { Prop, SchemaFactory } from '@nestjs/mongoose';
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Base } from '@/models/base.model';
import { BuildParts } from '@/models/build-parts.model';
import { Person } from '@/models/person.model';
import { Company } from '@/models/company.model';
import { UserType } from '@/models/user-type.model';
@ObjectType()
export class LivingSpaces extends Base {
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: BuildParts.name, required: true })
part: Types.ObjectId;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: Company.name, required: true })
company: Types.ObjectId;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: Person.name, required: true })
person: Types.ObjectId;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: UserType.name, required: true })
userType: Types.ObjectId;
}
export type LivingSpacesDocument = LivingSpaces & Document;
export const LivingSpacesSchema = SchemaFactory.createForClass(LivingSpaces);
export function getDynamicLivingSpaceModel(collectionToken: string, conn?: Connection): Model<LivingSpacesDocument> {
const connection = conn || defaultConnection;
const collectionName = `LivingSpaces${collectionToken}`;
if (connection.models[collectionName]) { return connection.models[collectionName] as Model<LivingSpacesDocument> }
const schema = SchemaFactory.createForClass(LivingSpaces);
schema.add({ uuid: { type: String, required: false, unique: true, default: () => randomUUID() } });
return connection.model(collectionName, schema, collectionName) as unknown as Model<LivingSpacesDocument>;
}

View File

@ -0,0 +1,51 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Base } from '@/models/base.model';
@Schema({ timestamps: true })
export class Person extends Base {
@Prop({ required: true })
firstName: string;
@Prop({ required: true })
surname: string;
@Prop({ required: true })
middleName: string;
@Prop({ required: true })
sexCode: string;
@Prop({ required: true })
personRef: string;
@Prop({ required: true })
personTag: string;
@Prop({ required: true })
fatherName: string;
@Prop({ required: true })
motherName: string;
@Prop({ required: true })
countryCode: string;
@Prop({ required: true })
nationalIdentityId: string;
@Prop({ required: true })
birthPlace: string;
@Prop({ required: true })
birthDate: Date;
@Prop({ required: true })
taxNo: string;
@Prop({ required: true })
birthname: string;
}
export type PersonDocument = Person & Document;
export const PersonSchema = SchemaFactory.createForClass(Person);

View File

@ -0,0 +1,26 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { ObjectType, Field, ID } from '@nestjs/graphql';
@ObjectType()
@Schema({ timestamps: true })
export class UserType {
@Field()
@Prop({ required: true })
type: string;
@Field()
@Prop({ required: true })
token: string;
@Field()
@Prop({ required: true })
typeToken: string;
@Field()
@Prop({ required: true })
description: string;
}
export type UserTypeDocument = UserType & Document;
export const UserTypeSchema = SchemaFactory.createForClass(UserType);

View File

@ -0,0 +1,81 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { Document, Types } from 'mongoose';
import { Base } from '@/models/base.model';
import { Person } from '@/models/person.model';
import { UserType } from '@/models/user-type.model';
@ObjectType()
export class CollectionTokenItem {
@Field()
@Prop({ required: true })
prefix: string;
@Field()
@Prop({ required: true })
token: string;
}
@ObjectType()
export class CollectionToken {
@Field(() => [CollectionTokenItem])
@Prop({ type: [CollectionTokenItem], default: [] })
tokens: CollectionTokenItem[];
@Field()
@Prop({ required: true, default: '' })
default: string;
}
@ObjectType()
@Schema({ timestamps: true })
export class User extends Base {
@Field(() => ID)
_id: string;
@Field({ nullable: true })
@Prop({ default: () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) })
expiresAt?: Date;
@Field({ nullable: true })
@Prop()
resetToken?: string;
@Field()
@Prop({ required: true })
password: string;
@Field(() => [String], { nullable: 'itemsAndList' })
@Prop({ type: [String], default: [], validate: [(val: string[]) => val.length <= 3, 'History can have max 3 items'] })
history?: string[];
@Field()
@Prop({ required: true })
tag: string;
@Field()
@Prop({ required: true, unique: true })
email: string;
@Field()
@Prop({ required: true, unique: true })
phone: string;
@Field(() => CollectionToken)
@Prop({ type: CollectionToken, default: () => ({ tokens: [], default: '' }) })
collectionTokens: CollectionToken;
@Field(() => ID)
@Prop({ type: Types.ObjectId, ref: Person.name, required: true })
person: Types.ObjectId;
@Field(() => ID, { nullable: true })
@Prop({ type: Types.ObjectId, ref: UserType.name })
type?: Types.ObjectId;
}
export type UserDocument = User & Document;
export const UserSchema = SchemaFactory.createForClass(User);

View File

@ -0,0 +1,7 @@
import { InputType, Field, ID } from '@nestjs/graphql';
@InputType()
export class CreatePersonInput {
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { PeopleResolver } from './people.resolver';
import { PeopleService } from './people.service';
import { MongooseModule } from '@nestjs/mongoose';
import { Person, PersonSchema } from '@/models/person.model';
@Module({
imports: [MongooseModule.forFeature([{ name: Person.name, schema: PersonSchema }])],
providers: [PeopleResolver, PeopleService]
})
export class PeopleModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PeopleResolver } from './people.resolver';
describe('PeopleResolver', () => {
let resolver: PeopleResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PeopleResolver],
}).compile();
resolver = module.get<PeopleResolver>(PeopleResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,29 @@
import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql';
import { Types } from 'mongoose';
import { Person } from '@/models/person.model';
import { PeopleService } from '@/people/people.service';
import { CreatePersonInput } from './dto/create-person.input';
import graphqlFields from 'graphql-fields';
import type { GraphQLResolveInfo } from 'graphql';
@Resolver()
export class PeopleResolver {
constructor(private readonly peopleService: PeopleService) { }
@Query(() => [Person], { name: 'Persons' })
async getPersons(@Info() info: GraphQLResolveInfo): Promise<Person[]> {
const fields = graphqlFields(info); const projection = this.peopleService.buildProjection(fields); return this.peopleService.findAll(projection);
}
@Query(() => Person, { name: 'Person', nullable: true })
async getPerson(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise<Person | null> {
const fields = graphqlFields(info); const projection = this.peopleService.buildProjection(fields); return this.peopleService.findById(new Types.ObjectId(id), projection);
}
@Mutation(() => Person, { name: 'createPerson' })
async createPerson(@Args('input') input: CreatePersonInput): Promise<Person> {
return this.peopleService.create(input);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PeopleService } from './people.service';
describe('PeopleService', () => {
let service: PeopleService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PeopleService],
}).compile();
service = module.get<PeopleService>(PeopleService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Types, Model } from 'mongoose';
import { Person, PersonDocument } from '@/models/person.model';
import { CreatePersonInput } from './dto/create-person.input';
@Injectable()
export class PeopleService {
constructor(
@InjectModel(Person.name) private readonly personModel: Model<PersonDocument>
) { }
async findAll(projection?: any): Promise<PersonDocument[]> {
return this.personModel.find({}, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec();
}
async findById(id: Types.ObjectId, projection?: any): Promise<PersonDocument | null> {
return this.personModel.findById(id, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec();
}
async create(input: CreatePersonInput): Promise<PersonDocument> {
const person = new this.personModel(input);
return person.save();
}
buildProjection(fields: Record<string, any>): any {
const projection: any = {};
for (const key in fields) {
if (key === 'person' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`person.${subField}`] = 1 } }
else { projection[key] = 1 }
}
return projection;
}
}

61
backend/src/schema.gql Normal file
View File

@ -0,0 +1,61 @@
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type CollectionTokenItem {
prefix: String!
token: String!
}
type CollectionToken {
tokens: [CollectionTokenItem!]!
default: String!
}
type User {
expiresAt: DateTime
resetToken: String
password: String!
history: [String]
tag: String!
email: String!
phone: String!
collectionTokens: CollectionToken!
person: ID!
type: ID
}
"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
input CreateUserInput {
password: String!
history: [String!]
tag: String!
email: String!
phone: String!
collectionTokens: CollectionTokenInput!
person: ID!
type: ID
}
input CollectionTokenInput {
tokens: [CollectionTokenItemInput!]!
default: String!
}
input CollectionTokenItemInput {
prefix: String!
token: String!
}

View File

@ -0,0 +1,7 @@
import { InputType, Field, ID } from '@nestjs/graphql';
@InputType()
export class CreateUserTypesInput {
}

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { UserTypesResolver } from './user-types.resolver';
import { UserTypesService } from './user-types.service';
@Module({
providers: [UserTypesResolver, UserTypesService]
})
export class UserTypesModule {}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserTypesResolver } from './user-types.resolver';
describe('UserTypesResolver', () => {
let resolver: UserTypesResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserTypesResolver],
}).compile();
resolver = module.get<UserTypesResolver>(UserTypesResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,29 @@
import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql';
import { Types } from 'mongoose';
import { UserType } from '@/models/user-type.model';
import { UserTypesService } from '@/user-types/user-types.service';
import type { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';
import { CreateUserTypesInput } from './dto/create-user-types.input';
@Resolver()
export class UserTypesResolver {
constructor(private readonly userTypesService: UserTypesService) { }
@Query(() => [UserType], { name: 'userTypes' })
async getUsers(@Info() info: GraphQLResolveInfo): Promise<UserType[]> {
const fields = graphqlFields(info); const projection = this.userTypesService.buildProjection(fields); return this.userTypesService.findAll(projection);
}
@Query(() => UserType, { name: 'userType', nullable: true })
async getUserType(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise<UserType | null> {
const fields = graphqlFields(info); const projection = this.userTypesService.buildProjection(fields); return this.userTypesService.findById(new Types.ObjectId(id), projection);
}
@Mutation(() => UserType, { name: 'createUserType' })
async createUserType(@Args('input') input: CreateUserTypesInput): Promise<UserType> {
return this.userTypesService.create(input);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserTypesService } from './user-types.service';
describe('UserTypesService', () => {
let service: UserTypesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserTypesService],
}).compile();
service = module.get<UserTypesService>(UserTypesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { CreateUserTypesInput } from './dto/create-user-types.input';
import { InjectModel } from '@nestjs/mongoose';
import { Types, Model } from 'mongoose';
import { UserType, UserTypeDocument } from '@/models/user-type.model';
@Injectable()
export class UserTypesService {
constructor(
@InjectModel(UserType.name) private readonly userTypesModel: Model<UserTypeDocument>
) { }
async findAll(projection?: any): Promise<UserTypeDocument[]> {
return this.userTypesModel.find({}, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec();
}
async findById(id: Types.ObjectId, projection?: any): Promise<UserTypeDocument | null> {
return this.userTypesModel.findById(id, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec();
}
async create(input: CreateUserTypesInput): Promise<UserTypeDocument> {
const user = new this.userTypesModel(input);
return user.save();
}
buildProjection(fields: Record<string, any>): any {
const projection: any = {};
for (const key in fields) {
if (key === 'user' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`user.${subField}`] = 1 } }
else { projection[key] = 1 }
}
return projection;
}
}

View File

@ -0,0 +1,47 @@
import { InputType, Field, ID } from '@nestjs/graphql';
@InputType()
export class CollectionTokenItemInput {
@Field()
prefix: string;
@Field()
token: string;
}
@InputType()
export class CollectionTokenInput {
@Field(() => [CollectionTokenItemInput])
tokens: CollectionTokenItemInput[];
@Field()
default: string;
}
@InputType()
export class CreateUserInput {
@Field()
password: string;
@Field(() => [String], { nullable: true })
history?: string[];
@Field()
tag: string;
@Field()
email: string;
@Field()
phone: string;
@Field(() => CollectionTokenInput)
collectionTokens: CollectionTokenInput;
@Field(() => ID)
person: string;
@Field(() => ID, { nullable: true })
type?: string;
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';
import { UserSchema, User } from '@/models/user.model';
@Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
providers: [UsersResolver, UsersService],
})
export class UsersModule { }

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersResolver } from './users.resolver';
describe('UsersResolver', () => {
let resolver: UsersResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersResolver],
}).compile();
resolver = module.get<UsersResolver>(UsersResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,29 @@
import { Resolver, Query, Args, ID, Info, Mutation } from '@nestjs/graphql';
import { Types } from 'mongoose';
import { User } from '@/models/user.model';
import { UsersService } from '@/users/users.service';
import type { GraphQLResolveInfo } from 'graphql';
import graphqlFields from 'graphql-fields';
import { CreateUserInput } from './dto/create-user.input';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) { }
@Query(() => [User], { name: 'users' })
async getUsers(@Info() info: GraphQLResolveInfo): Promise<User[]> {
const fields = graphqlFields(info); const projection = this.usersService.buildProjection(fields); return this.usersService.findAll(projection);
}
@Query(() => User, { name: 'user', nullable: true })
async getUser(@Args('id', { type: () => ID }) id: string, @Info() info: GraphQLResolveInfo): Promise<User | null> {
const fields = graphqlFields(info); const projection = this.usersService.buildProjection(fields); return this.usersService.findById(new Types.ObjectId(id), projection);
}
@Mutation(() => User, { name: 'createUser' })
async createUser(@Args('input') input: CreateUserInput): Promise<User> {
return this.usersService.create(input);
}
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Types, Model } from 'mongoose';
import { User, UserDocument } from '@/models/user.model';
import { CreateUserInput } from './dto/create-user.input';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private readonly userModel: Model<UserDocument>
) { }
async findAll(projection?: any): Promise<UserDocument[]> {
return this.userModel.find({}, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec();
}
async findById(id: Types.ObjectId, projection?: any): Promise<UserDocument | null> {
return this.userModel.findById(id, projection, { lean: false }).populate({ path: 'person', select: projection?.person }).exec();
}
async create(input: CreateUserInput): Promise<UserDocument> {
const user = new this.userModel(input);
return user.save();
}
buildProjection(fields: Record<string, any>): any {
const projection: any = {};
for (const key in fields) {
if (key === 'person' && typeof fields[key] === 'object') { for (const subField of Object.keys(fields[key])) { projection[`person.${subField}`] = 1 } }
else { projection[key] = 1 }
}
return projection;
}
}

View File

@ -0,0 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

30
backend/tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false,
"paths": {
"@/*": [
"src/*"
]
}
}
}

View File

@ -1,6 +1,7 @@
services:
app:
container_name: evyos-frontend
frontend-evyos:
container_name: frontend-evyos
build:
context: .
dockerfile: Dockerfile
@ -10,30 +11,23 @@ services:
- dbConnect
env_file:
- .env
depends_on:
- storage
volumes:
- .:/app
- /app/node_modules
storage:
image: mongo:6
container_name: storage
restart: always
apollo-server:
container_name: apollo-server
build:
context: .
dockerfile: ql-server/Dockerfile
ports:
- "4000:4000"
networks:
- dbConnect
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: secret123
MONGO_INITDB_DATABASE: evyosdb
ports:
- "27017:27017"
volumes:
- storage_data:/data/db
env_file:
- .env
# restart: unless-stopped
networks:
dbConnect:
driver: bridge
volumes:
storage_data:

View File

View File

@ -0,0 +1,40 @@
import { AppSidebar } from "@/components/app-sidebar"
import { ChartAreaInteractive } from "@/components/chart-area-interactive"
import { DataTable } from "@/components/data-table"
import { SectionCards } from "@/components/section-cards"
import { SiteHeader } from "@/components/site-header"
import {
SidebarInset,
SidebarProvider,
} from "@/frontend/components/ui/sidebar"
import data from "./data.json"
export default function Page() {
return (
<SidebarProvider
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
"--header-height": "calc(var(--spacing) * 12)",
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<SectionCards />
<div className="px-4 lg:px-6">
<ChartAreaInteractive />
</div>
<DataTable data={data} />
</div>
</div>
</div>
</SidebarInset>
</SidebarProvider>
)
}

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,5 @@
import PageMock from "@/pages/mock/page";
const MockPage = () => { return <div><PageMock /></div> };
export default MockPage;

View File

@ -0,0 +1,182 @@
"use client"
import * as React from "react"
import {
IconCamera,
IconChartBar,
IconDashboard,
IconDatabase,
IconFileAi,
IconFileDescription,
IconFileWord,
IconFolder,
IconHelp,
IconInnerShadowTop,
IconListDetails,
IconReport,
IconSearch,
IconSettings,
IconUsers,
} from "@tabler/icons-react"
import { NavMain } from "@/frontend/components/dashboard/nav-main"
import { NavSecondary } from "@/frontend/components/dashboard/nav-secondary"
import { NavUser } from "@/frontend/components/dashboard/nav-user"
import { NavDocuments } from "@/frontend/components/dashboard/nav-documents"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/frontend/components/ui/sidebar"
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
navMain: [
{
title: "Dashboard",
url: "#",
icon: IconDashboard,
},
{
title: "Lifecycle",
url: "#",
icon: IconListDetails,
},
{
title: "Analytics",
url: "#",
icon: IconChartBar,
},
{
title: "Projects",
url: "#",
icon: IconFolder,
},
{
title: "Team",
url: "#",
icon: IconUsers,
},
],
navClouds: [
{
title: "Capture",
icon: IconCamera,
isActive: true,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Proposal",
icon: IconFileDescription,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
{
title: "Prompts",
icon: IconFileAi,
url: "#",
items: [
{
title: "Active Proposals",
url: "#",
},
{
title: "Archived",
url: "#",
},
],
},
],
navSecondary: [
{
title: "Settings",
url: "#",
icon: IconSettings,
},
{
title: "Get Help",
url: "#",
icon: IconHelp,
},
{
title: "Search",
url: "#",
icon: IconSearch,
},
],
documents: [
{
name: "Data Library",
url: "#",
icon: IconDatabase,
},
{
name: "Reports",
url: "#",
icon: IconReport,
},
{
name: "Word Assistant",
url: "#",
icon: IconFileWord,
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="offcanvas" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
className="data-[slot=sidebar-menu-button]:p-1.5!"
>
<a href="#">
<IconInnerShadowTop className="size-5!" />
<span className="text-base font-semibold">Acme Inc.</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavDocuments items={data.documents} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
)
}

View File

@ -0,0 +1,291 @@
"use client"
import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { useIsMobile } from "@/hooks/use-mobile"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/frontend/components/ui/card"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/frontend/components/ui/chart"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/frontend/components/ui/select"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/frontend/components/ui/toggle-group"
export const description = "An interactive area chart"
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
const chartConfig = {
visitors: {
label: "Visitors",
},
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("90d")
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
}
}, [isMobile])
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)
const referenceDate = new Date("2024-06-30")
let daysToSubtract = 90
if (timeRange === "30d") {
daysToSubtract = 30
} else if (timeRange === "7d") {
daysToSubtract = 7
}
const startDate = new Date(referenceDate)
startDate.setDate(startDate.getDate() - daysToSubtract)
return date >= startDate
})
return (
<Card className="@container/card">
<CardHeader>
<CardTitle>Total Visitors</CardTitle>
<CardDescription>
<span className="hidden @[540px]/card:block">
Total for the last 3 months
</span>
<span className="@[540px]/card:hidden">Last 3 months</span>
</CardDescription>
<CardAction>
<ToggleGroup
type="single"
value={timeRange}
onValueChange={setTimeRange}
variant="outline"
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
>
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
</ToggleGroup>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
size="sm"
aria-label="Select a value"
>
<SelectValue placeholder="Last 3 months" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="90d" className="rounded-lg">
Last 3 months
</SelectItem>
<SelectItem value="30d" className="rounded-lg">
Last 30 days
</SelectItem>
<SelectItem value="7d" className="rounded-lg">
Last 7 days
</SelectItem>
</SelectContent>
</Select>
</CardAction>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={filteredData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={1.0}
/>
<stop
offset="95%"
stopColor="var(--color-desktop)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-mobile)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-mobile)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
indicator="dot"
/>
}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,807 @@
"use client"
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type DragEndEvent,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconCircleCheckFilled,
IconDotsVertical,
IconGripVertical,
IconLayoutColumns,
IconLoader,
IconPlus,
IconTrendingUp,
} from "@tabler/icons-react"
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { toast } from "sonner"
import { z } from "zod"
import { useIsMobile } from "@/hooks/use-mobile"
import { Badge } from "@/frontend/components/ui/badge"
import { Button } from "@/frontend/components/ui/button"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/frontend/components/ui/chart"
import { Checkbox } from "@/frontend/components/ui/checkbox"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/frontend/components/ui/drawer"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/frontend/components/ui/dropdown-menu"
import { Input } from "@/frontend/components/ui/input"
import { Label } from "@/frontend/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/frontend/components/ui/select"
import { Separator } from "@/frontend/components/ui/separator"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/frontend/components/ui/table"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/frontend/components/ui/tabs"
export const schema = z.object({
id: z.number(),
header: z.string(),
type: z.string(),
status: z.string(),
target: z.string(),
limit: z.string(),
reviewer: z.string(),
})
// Create a separate component for the drag handle
function DragHandle({ id }: { id: number }) {
const { attributes, listeners } = useSortable({
id,
})
return (
<Button
{...attributes}
{...listeners}
variant="ghost"
size="icon"
className="text-muted-foreground size-7 hover:bg-transparent"
>
<IconGripVertical className="text-muted-foreground size-3" />
<span className="sr-only">Drag to reorder</span>
</Button>
)
}
const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
id: "drag",
header: () => null,
cell: ({ row }) => <DragHandle id={row.original.id} />,
},
{
id: "select",
header: ({ table }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
</div>
),
cell: ({ row }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
</div>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "header",
header: "Header",
cell: ({ row }) => {
return <TableCellViewer item={row.original} />
},
enableHiding: false,
},
{
accessorKey: "type",
header: "Section Type",
cell: ({ row }) => (
<div className="w-32">
<Badge variant="outline" className="text-muted-foreground px-1.5">
{row.original.type}
</Badge>
</div>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<Badge variant="outline" className="text-muted-foreground px-1.5">
{row.original.status === "Done" ? (
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
) : (
<IconLoader />
)}
{row.original.status}
</Badge>
),
},
{
accessorKey: "target",
header: () => <div className="w-full text-right">Target</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault()
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
})
}}
>
<Label htmlFor={`${row.original.id}-target`} className="sr-only">
Target
</Label>
<Input
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
defaultValue={row.original.target}
id={`${row.original.id}-target`}
/>
</form>
),
},
{
accessorKey: "limit",
header: () => <div className="w-full text-right">Limit</div>,
cell: ({ row }) => (
<form
onSubmit={(e) => {
e.preventDefault()
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
loading: `Saving ${row.original.header}`,
success: "Done",
error: "Error",
})
}}
>
<Label htmlFor={`${row.original.id}-limit`} className="sr-only">
Limit
</Label>
<Input
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
defaultValue={row.original.limit}
id={`${row.original.id}-limit`}
/>
</form>
),
},
{
accessorKey: "reviewer",
header: "Reviewer",
cell: ({ row }) => {
const isAssigned = row.original.reviewer !== "Assign reviewer"
if (isAssigned) {
return row.original.reviewer
}
return (
<>
<Label htmlFor={`${row.original.id}-reviewer`} className="sr-only">
Reviewer
</Label>
<Select>
<SelectTrigger
className="w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate"
size="sm"
id={`${row.original.id}-reviewer`}
>
<SelectValue placeholder="Assign reviewer" />
</SelectTrigger>
<SelectContent align="end">
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">
Jamik Tashpulatov
</SelectItem>
</SelectContent>
</Select>
</>
)
},
},
{
id: "actions",
cell: () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
size="icon"
>
<IconDotsVertical />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
]
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({
id: row.original.id,
})
return (
<TableRow
data-state={row.getIsSelected() && "selected"}
data-dragging={isDragging}
ref={setNodeRef}
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
style={{
transform: CSS.Transform.toString(transform),
transition: transition,
}}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}
export function DataTable({
data: initialData,
}: {
data: z.infer<typeof schema>[]
}) {
const [data, setData] = React.useState(() => initialData)
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [sorting, setSorting] = React.useState<SortingState>([])
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
})
const sortableId = React.useId()
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
)
const dataIds = React.useMemo<UniqueIdentifier[]>(
() => data?.map(({ id }) => id) || [],
[data]
)
const table = useReactTable({
data,
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination,
},
getRowId: (row) => row.id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (active && over && active.id !== over.id) {
setData((data) => {
const oldIndex = dataIds.indexOf(active.id)
const newIndex = dataIds.indexOf(over.id)
return arrayMove(data, oldIndex, newIndex)
})
}
}
return (
<Tabs
defaultValue="outline"
className="w-full flex-col justify-start gap-6"
>
<div className="flex items-center justify-between px-4 lg:px-6">
<Label htmlFor="view-selector" className="sr-only">
View
</Label>
<Select defaultValue="outline">
<SelectTrigger
className="flex w-fit @4xl/main:hidden"
size="sm"
id="view-selector"
>
<SelectValue placeholder="Select a view" />
</SelectTrigger>
<SelectContent>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="past-performance">Past Performance</SelectItem>
<SelectItem value="key-personnel">Key Personnel</SelectItem>
<SelectItem value="focus-documents">Focus Documents</SelectItem>
</SelectContent>
</Select>
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
<TabsTrigger value="outline">Outline</TabsTrigger>
<TabsTrigger value="past-performance">
Past Performance <Badge variant="secondary">3</Badge>
</TabsTrigger>
<TabsTrigger value="key-personnel">
Key Personnel <Badge variant="secondary">2</Badge>
</TabsTrigger>
<TabsTrigger value="focus-documents">Focus Documents</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<IconLayoutColumns />
<span className="hidden lg:inline">Customize Columns</span>
<span className="lg:hidden">Columns</span>
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" &&
column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm">
<IconPlus />
<span className="hidden lg:inline">Add Section</span>
</Button>
</div>
</div>
<TabsContent
value="outline"
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
>
<div className="overflow-hidden rounded-lg border">
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}
id={sortableId}
>
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{table.getRowModel().rows?.length ? (
<SortableContext
items={dataIds}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map((row) => (
<DraggableRow key={row.id} row={row} />
))}
</SortableContext>
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label htmlFor="rows-per-page" className="text-sm font-medium">
Rows per page
</Label>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
<SelectValue
placeholder={table.getState().pagination.pageSize}
/>
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<IconChevronLeft />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<IconChevronRight />
</Button>
<Button
variant="outline"
className="hidden size-8 lg:flex"
size="icon"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent
value="past-performance"
className="flex flex-col px-4 lg:px-6"
>
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
<TabsContent
value="focus-documents"
className="flex flex-col px-4 lg:px-6"
>
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
</TabsContent>
</Tabs>
)
}
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
const isMobile = useIsMobile()
return (
<Drawer direction={isMobile ? "bottom" : "right"}>
<DrawerTrigger asChild>
<Button variant="link" className="text-foreground w-fit px-0 text-left">
{item.header}
</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="gap-1">
<DrawerTitle>{item.header}</DrawerTitle>
<DrawerDescription>
Showing total visitors for the last 6 months
</DrawerDescription>
</DrawerHeader>
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
{!isMobile && (
<>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 10,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
hide
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.6}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
<Separator />
<div className="grid gap-2">
<div className="flex gap-2 leading-none font-medium">
Trending up by 5.2% this month{" "}
<IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Showing total visitors for the last 6 months. This is just
some random text to test the layout. It spans multiple lines
and should wrap around.
</div>
</div>
<Separator />
</>
)}
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="header">Header</Label>
<Input id="header" defaultValue={item.header} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="type">Type</Label>
<Select defaultValue={item.type}>
<SelectTrigger id="type" className="w-full">
<SelectValue placeholder="Select a type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Table of Contents">
Table of Contents
</SelectItem>
<SelectItem value="Executive Summary">
Executive Summary
</SelectItem>
<SelectItem value="Technical Approach">
Technical Approach
</SelectItem>
<SelectItem value="Design">Design</SelectItem>
<SelectItem value="Capabilities">Capabilities</SelectItem>
<SelectItem value="Focus Documents">
Focus Documents
</SelectItem>
<SelectItem value="Narrative">Narrative</SelectItem>
<SelectItem value="Cover Page">Cover Page</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="status">Status</Label>
<Select defaultValue={item.status}>
<SelectTrigger id="status" className="w-full">
<SelectValue placeholder="Select a status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Done">Done</SelectItem>
<SelectItem value="In Progress">In Progress</SelectItem>
<SelectItem value="Not Started">Not Started</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="target">Target</Label>
<Input id="target" defaultValue={item.target} />
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="limit">Limit</Label>
<Input id="limit" defaultValue={item.limit} />
</div>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="reviewer">Reviewer</Label>
<Select defaultValue={item.reviewer}>
<SelectTrigger id="reviewer" className="w-full">
<SelectValue placeholder="Select a reviewer" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Eddie Lake">Eddie Lake</SelectItem>
<SelectItem value="Jamik Tashpulatov">
Jamik Tashpulatov
</SelectItem>
<SelectItem value="Emily Whalen">Emily Whalen</SelectItem>
</SelectContent>
</Select>
</div>
</form>
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose asChild>
<Button variant="outline">Done</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}

View File

@ -0,0 +1,92 @@
"use client"
import {
IconDots,
IconFolder,
IconShare3,
IconTrash,
type Icon,
} from "@tabler/icons-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/frontend/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/frontend/components/ui/sidebar"
export function NavDocuments({
items,
}: {
items: {
name: string
url: string
icon: Icon
}[]
}) {
const { isMobile } = useSidebar()
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Documents</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="data-[state=open]:bg-accent rounded-sm"
>
<IconDots />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-24 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<IconFolder />
<span>Open</span>
</DropdownMenuItem>
<DropdownMenuItem>
<IconShare3 />
<span>Share</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<IconTrash />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<IconDots className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}

View File

@ -0,0 +1,58 @@
"use client"
import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
import { Button } from "@/frontend/components/ui/button"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/frontend/components/ui/sidebar"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon?: Icon
}[]
}) {
return (
<SidebarGroup>
<SidebarGroupContent className="flex flex-col gap-2">
<SidebarMenu>
<SidebarMenuItem className="flex items-center gap-2">
<SidebarMenuButton
tooltip="Quick Create"
className="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
>
<IconCirclePlusFilled />
<span>Quick Create</span>
</SidebarMenuButton>
<Button
size="icon"
className="size-8 group-data-[collapsible=icon]:opacity-0"
variant="outline"
>
<IconMail />
<span className="sr-only">Inbox</span>
</Button>
</SidebarMenuItem>
</SidebarMenu>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}

View File

@ -0,0 +1,42 @@
"use client"
import * as React from "react"
import { type Icon } from "@tabler/icons-react"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/frontend/components/ui/sidebar"
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: Icon
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}

View File

@ -0,0 +1,110 @@
"use client"
import {
IconCreditCard,
IconDotsVertical,
IconLogout,
IconNotification,
IconUserCircle,
} from "@tabler/icons-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/frontend/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/frontend/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/frontend/components/ui/sidebar"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg grayscale">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
</div>
<IconDotsVertical className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconUserCircle />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<IconCreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<IconNotification />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<IconLogout />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@ -0,0 +1,102 @@
import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
import { Badge } from "@/frontend/components/ui/badge"
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/frontend/components/ui/card"
export function SectionCards() {
return (
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-linear-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
<Card className="@container/card">
<CardHeader>
<CardDescription>Total Revenue</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
$1,250.00
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Trending up this month <IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
Visitors for the last 6 months
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>New Customers</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
1,234
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingDown />
-20%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Down 20% this period <IconTrendingDown className="size-4" />
</div>
<div className="text-muted-foreground">
Acquisition needs attention
</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
45,678
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Strong user retention <IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">Engagement exceed targets</div>
</CardFooter>
</Card>
<Card className="@container/card">
<CardHeader>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
4.5%
</CardTitle>
<CardAction>
<Badge variant="outline">
<IconTrendingUp />
+4.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
Steady performance increase <IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">Meets growth projections</div>
</CardFooter>
</Card>
</div>
)
}

View File

@ -0,0 +1,30 @@
import { Button } from "@/frontend/components/ui/button"
import { Separator } from "@/frontend/components/ui/separator"
import { SidebarTrigger } from "@/frontend/components/ui/sidebar"
export function SiteHeader() {
return (
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" />
<Separator
orientation="vertical"
className="mx-2 data-[orientation=vertical]:h-4"
/>
<h1 className="text-base font-medium">Documents</h1>
<div className="ml-auto flex items-center gap-2">
<Button variant="ghost" asChild size="sm" className="hidden sm:flex">
<a
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
rel="noopener noreferrer"
target="_blank"
className="dark:text-foreground"
>
GitHub
</a>
</Button>
</div>
</div>
</header>
)
}

View File

View File

@ -0,0 +1,53 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,60 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,357 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

Some files were not shown because too many files have changed in this diff Show More