Initialize Hot Desking app with web and API services

- Set up SvelteKit for the web interface with Tailwind CSS.
- Build the API using NestJS with Prisma ORM for database interaction.
- Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances.
- Create shared data models for consistent typing across services.
- Establish basic routing and layouts for web and API services.
main
Pau Costa Ferrer 2026-03-28 00:06:57 +01:00
commit 1faa0d5c8f
81 changed files with 12844 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
/tmp
/out-tsc
/node_modules
**/node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.pnp
.pnp.js
.vscode/*
.idea/*
**/.idea
**/.svelte-kit

10
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

132
.idea/workspace.xml Normal file
View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change afterPath="$PROJECT_DIR$/prompts.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/index.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/tsconfig.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="EmbeddingIndexingInfo">
<option name="cachedIndexableFilesCount" value="10" />
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="McpProjectServerCommands">
<commands />
<urls />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 0,
&quot;fromUser&quot;: false
}</component>
<component name="ProjectId" id="3BXlUbAsiEmEvH0PacGIZgCuvpY" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Docker.docker-compose.yml: Compose Deployment.executor": "Run",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.MCP Project settings loaded": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"codeWithMe.voiceChat.enabledByDefault": "false",
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
"git-widget-placeholder": "main",
"javascript.preferred.runtime.type.id": "node",
"junie.onboarding.icon.badge.shown": "true",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_interpreter_path": "/home/micosil/.nvm/versions/node/v22.21.1/bin/node",
"nodejs_package_manager_path": "npm",
"npm.api > start:dev.executor": "Run",
"npm.web > dev.executor": "Run",
"settings.editor.selected.configurable": "settings.javascript.runtime",
"to.speed.mode.migration.done": "true",
"ts.external.directory.path": "/home/micosil/WebstormProjects/desker/node_modules/typescript/lib",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager" selected="Docker.docker-compose.yml: Compose Deployment">
<configuration default="true" type="docker-deploy" factoryName="docker-compose.yml" temporary="true">
<deployment type="docker-compose.yml" />
<method v="2" />
</configuration>
<configuration name="docker-compose.yml: Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" temporary="true" server-name="Docker">
<deployment type="docker-compose.yml">
<settings>
<option name="sourceFilePath" value="docker-compose.yml" />
</settings>
</deployment>
<method v="2" />
</configuration>
<configuration name="api &gt; start:dev" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/apps/api/package.json" />
<command value="run" />
<scripts>
<script value="start:dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
<configuration name="web &gt; dev" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/apps/web/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="$USER_HOME$/.nvm/versions/node/v22.21.1/bin/node" />
<envs />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="npm.api &gt; start:dev" />
<item itemvalue="Docker.docker-compose.yml: Compose Deployment" />
<item itemvalue="npm.web &gt; dev" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-3bd3a6803838-JavaScript-WS-261.22158.274" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="" />
<created>1774643312599</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1774643312599</updated>
<workItem from="1774643313611" duration="74000" />
<workItem from="1774643422240" duration="2753000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

2
apps/api/.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/hotdesking"
PORT=3000

4
apps/api/.prettierrc Normal file
View File

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

98
apps/api/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).

6
apps/api/dist/app.controller.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import { AppService } from './app.service';
export declare class AppController {
private readonly appService;
constructor(appService: AppService);
getHello(): string;
}

35
apps/api/dist/app.controller.js vendored Normal file
View File

@ -0,0 +1,35 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppController = void 0;
const common_1 = require("@nestjs/common");
const app_service_1 = require("./app.service");
let AppController = class AppController {
appService;
constructor(appService) {
this.appService = appService;
}
getHello() {
return this.appService.getHello();
}
};
exports.AppController = AppController;
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", String)
], AppController.prototype, "getHello", null);
exports.AppController = AppController = __decorate([
(0, common_1.Controller)(),
__metadata("design:paramtypes", [app_service_1.AppService])
], AppController);
//# sourceMappingURL=app.controller.js.map

1
apps/api/dist/app.controller.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["../src/app.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,+CAA2C;AAGpC,IAAM,aAAa,GAAnB,MAAM,aAAa;IACK;IAA7B,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAGvD,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;CACF,CAAA;AAPY,sCAAa;AAIxB;IADC,IAAA,YAAG,GAAE;;;;6CAGL;wBANU,aAAa;IADzB,IAAA,mBAAU,GAAE;qCAE8B,wBAAU;GADxC,aAAa,CAOzB"}

2
apps/api/dist/app.module.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
export declare class AppModule {
}

25
apps/api/dist/app.module.js vendored Normal file
View File

@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppModule = void 0;
const common_1 = require("@nestjs/common");
const app_controller_1 = require("./app.controller");
const app_service_1 = require("./app.service");
const prisma_module_1 = require("./prisma/prisma.module");
const spaces_module_1 = require("./spaces/spaces.module");
let AppModule = class AppModule {
};
exports.AppModule = AppModule;
exports.AppModule = AppModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule, spaces_module_1.SpacesModule],
controllers: [app_controller_1.AppController],
providers: [app_service_1.AppService],
})
], AppModule);
//# sourceMappingURL=app.module.js.map

1
apps/api/dist/app.module.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAC3C,0DAAsD;AACtD,0DAAsD;AAO/C,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IALrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,4BAAY,CAAC;QACrC,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,CAAC;KACxB,CAAC;GACW,SAAS,CAAG"}

3
apps/api/dist/app.service.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export declare class AppService {
getHello(): string;
}

20
apps/api/dist/app.service.js vendored Normal file
View File

@ -0,0 +1,20 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppService = void 0;
const common_1 = require("@nestjs/common");
let AppService = class AppService {
getHello() {
return 'Hello World!';
}
};
exports.AppService = AppService;
exports.AppService = AppService = __decorate([
(0, common_1.Injectable)()
], AppService);
//# sourceMappingURL=app.service.js.map

1
apps/api/dist/app.service.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"app.service.js","sourceRoot":"","sources":["../src/app.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAGrC,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,QAAQ;QACN,OAAO,cAAc,CAAC;IACxB,CAAC;CACF,CAAA;AAJY,gCAAU;qBAAV,UAAU;IADtB,IAAA,mBAAU,GAAE;GACA,UAAU,CAItB"}

View File

@ -0,0 +1,4 @@
import { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
export declare class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void;
}

View File

@ -0,0 +1,35 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpExceptionFilter = void 0;
const common_1 = require("@nestjs/common");
let HttpExceptionFilter = class HttpExceptionFilter {
catch(exception, host) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const isHttpException = exception instanceof common_1.HttpException;
const status = isHttpException
? exception.getStatus()
: common_1.HttpStatus.INTERNAL_SERVER_ERROR;
const message = isHttpException
? exception.getResponse()
: 'Internal server error';
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
};
exports.HttpExceptionFilter = HttpExceptionFilter;
exports.HttpExceptionFilter = HttpExceptionFilter = __decorate([
(0, common_1.Catch)()
], HttpExceptionFilter);
//# sourceMappingURL=http-exception.filter.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"http-exception.filter.js","sourceRoot":"","sources":["../../../src/common/filters/http-exception.filter.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAMwB;AAIjB,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,KAAK,CAAC,SAAkB,EAAE,IAAmB;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAY,CAAC;QAC7C,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,EAAW,CAAC;QAE1C,MAAM,eAAe,GAAG,SAAS,YAAY,sBAAa,CAAC;QAE3D,MAAM,MAAM,GAAG,eAAe;YAC5B,CAAC,CAAC,SAAS,CAAC,SAAS,EAAE;YACvB,CAAC,CAAC,mBAAU,CAAC,qBAAqB,CAAC;QAErC,MAAM,OAAO,GAAG,eAAe;YAC7B,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;YACzB,CAAC,CAAC,uBAAuB,CAAC;QAE5B,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC;YAC3B,UAAU,EAAE,MAAM;YAClB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,IAAI,EAAE,OAAO,CAAC,GAAG;YACjB,OAAO;SACR,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AAvBY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,cAAK,GAAE;GACK,mBAAmB,CAuB/B"}

1
apps/api/dist/main.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

22
apps/api/dist/main.js vendored Normal file
View File

@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const common_1 = require("@nestjs/common");
const core_1 = require("@nestjs/core");
const app_module_1 = require("./app.module");
const http_exception_filter_1 = require("./common/filters/http-exception.filter");
async function bootstrap() {
const app = await core_1.NestFactory.create(app_module_1.AppModule);
app.useGlobalPipes(new common_1.ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}));
app.useGlobalFilters(new http_exception_filter_1.HttpExceptionFilter());
app.enableCors({
origin: ['http://localhost:5173'],
credentials: true,
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
//# sourceMappingURL=main.js.map

1
apps/api/dist/main.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,2CAAgD;AAChD,uCAA2C;AAC3C,6CAAyC;AACzC,kFAA6E;AAE7E,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAC;IAEhD,GAAG,CAAC,cAAc,CAChB,IAAI,uBAAc,CAAC;QACjB,SAAS,EAAE,IAAI;QACf,SAAS,EAAE,IAAI;QACf,oBAAoB,EAAE,IAAI;KAC3B,CAAC,CACH,CAAC;IAEF,GAAG,CAAC,gBAAgB,CAAC,IAAI,2CAAmB,EAAE,CAAC,CAAC;IAEhD,GAAG,CAAC,UAAU,CAAC;QACb,MAAM,EAAE,CAAC,uBAAuB,CAAC;QACjC,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAEH,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;AAC7C,CAAC;AACD,SAAS,EAAE,CAAC"}

View File

@ -0,0 +1,2 @@
export declare class PrismaModule {
}

22
apps/api/dist/prisma/prisma.module.js vendored Normal file
View File

@ -0,0 +1,22 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrismaModule = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("./prisma.service");
let PrismaModule = class PrismaModule {
};
exports.PrismaModule = PrismaModule;
exports.PrismaModule = PrismaModule = __decorate([
(0, common_1.Global)(),
(0, common_1.Module)({
providers: [prisma_service_1.PrismaService],
exports: [prisma_service_1.PrismaService],
})
], PrismaModule);
//# sourceMappingURL=prisma.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"prisma.module.js","sourceRoot":"","sources":["../../src/prisma/prisma.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAgD;AAChD,qDAAiD;AAO1C,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IALxB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,SAAS,EAAE,CAAC,8BAAa,CAAC;QAC1B,OAAO,EAAE,CAAC,8BAAa,CAAC;KACzB,CAAC;GACW,YAAY,CAAG"}

View File

@ -0,0 +1,6 @@
import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
export declare class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
onModuleInit(): Promise<void>;
onModuleDestroy(): Promise<void>;
}

24
apps/api/dist/prisma/prisma.service.js vendored Normal file
View File

@ -0,0 +1,24 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrismaService = void 0;
const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
let PrismaService = class PrismaService extends client_1.PrismaClient {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
};
exports.PrismaService = PrismaService;
exports.PrismaService = PrismaService = __decorate([
(0, common_1.Injectable)()
], PrismaService);
//# sourceMappingURL=prisma.service.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"prisma.service.js","sourceRoot":"","sources":["../../src/prisma/prisma.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA2E;AAC3E,2CAA8C;AAGvC,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,qBAAY;IAC7C,KAAK,CAAC,YAAY;QAChB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IAC3B,CAAC;CACF,CAAA;AARY,sCAAa;wBAAb,aAAa;IADzB,IAAA,mBAAU,GAAE;GACA,aAAa,CAQzB"}

View File

@ -0,0 +1,6 @@
import { SpacesService } from './spaces.service';
export declare class SpacesController {
private readonly spacesService;
constructor(spacesService: SpacesService);
findAll(): import("@hot-desking/shared").SpaceDTO[];
}

View File

@ -0,0 +1,35 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpacesController = void 0;
const common_1 = require("@nestjs/common");
const spaces_service_1 = require("./spaces.service");
let SpacesController = class SpacesController {
spacesService;
constructor(spacesService) {
this.spacesService = spacesService;
}
findAll() {
return this.spacesService.findAll();
}
};
exports.SpacesController = SpacesController;
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], SpacesController.prototype, "findAll", null);
exports.SpacesController = SpacesController = __decorate([
(0, common_1.Controller)('spaces'),
__metadata("design:paramtypes", [spaces_service_1.SpacesService])
], SpacesController);
//# sourceMappingURL=spaces.controller.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"spaces.controller.js","sourceRoot":"","sources":["../../src/spaces/spaces.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,qDAAiD;AAG1C,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IACE;IAA7B,YAA6B,aAA4B;QAA5B,kBAAa,GAAb,aAAa,CAAe;IAAG,CAAC;IAG7D,OAAO;QACL,OAAO,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;IACtC,CAAC;CACF,CAAA;AAPY,4CAAgB;AAI3B;IADC,IAAA,YAAG,GAAE;;;;+CAGL;2BANU,gBAAgB;IAD5B,IAAA,mBAAU,EAAC,QAAQ,CAAC;qCAEyB,8BAAa;GAD9C,gBAAgB,CAO5B"}

View File

@ -0,0 +1,2 @@
export declare class SpacesModule {
}

22
apps/api/dist/spaces/spaces.module.js vendored Normal file
View File

@ -0,0 +1,22 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpacesModule = void 0;
const common_1 = require("@nestjs/common");
const spaces_controller_1 = require("./spaces.controller");
const spaces_service_1 = require("./spaces.service");
let SpacesModule = class SpacesModule {
};
exports.SpacesModule = SpacesModule;
exports.SpacesModule = SpacesModule = __decorate([
(0, common_1.Module)({
controllers: [spaces_controller_1.SpacesController],
providers: [spaces_service_1.SpacesService],
})
], SpacesModule);
//# sourceMappingURL=spaces.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"spaces.module.js","sourceRoot":"","sources":["../../src/spaces/spaces.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,qDAAiD;AAM1C,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IAJxB,IAAA,eAAM,EAAC;QACN,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,CAAC,8BAAa,CAAC;KAC3B,CAAC;GACW,YAAY,CAAG"}

View File

@ -0,0 +1,4 @@
import { SpaceDTO } from '@hot-desking/shared';
export declare class SpacesService {
findAll(): SpaceDTO[];
}

30
apps/api/dist/spaces/spaces.service.js vendored Normal file
View File

@ -0,0 +1,30 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpacesService = void 0;
const common_1 = require("@nestjs/common");
const shared_1 = require("@hot-desking/shared");
let SpacesService = class SpacesService {
findAll() {
return [
{
id: '1',
name: 'Desk 1',
type: shared_1.SpaceType.Desk,
x: 10,
y: 20,
status: shared_1.BookingStatus.Available,
},
];
}
};
exports.SpacesService = SpacesService;
exports.SpacesService = SpacesService = __decorate([
(0, common_1.Injectable)()
], SpacesService);
//# sourceMappingURL=spaces.service.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"spaces.service.js","sourceRoot":"","sources":["../../src/spaces/spaces.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAC5C,gDAAyE;AAGlE,IAAM,aAAa,GAAnB,MAAM,aAAa;IACxB,OAAO;QACL,OAAO;YACL;gBACE,EAAE,EAAE,GAAG;gBACP,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,kBAAS,CAAC,IAAI;gBACpB,CAAC,EAAE,EAAE;gBACL,CAAC,EAAE,EAAE;gBACL,MAAM,EAAE,sBAAa,CAAC,SAAS;aAChC;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AAbY,sCAAa;wBAAb,aAAa;IADzB,IAAA,mBAAU,GAAE;GACA,aAAa,CAazB"}

File diff suppressed because one or more lines are too long

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
apps/api/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
}
}

75
apps/api/package.json Normal file
View File

@ -0,0 +1,75 @@
{
"name": "api",
"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": {
"@nestjs/common": "^10.4.22",
"@nestjs/core": "^10.4.22",
"@nestjs/platform-express": "^10.4.22",
"@prisma/client": "^6.4.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"prisma": "^6.4.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.22",
"@types/express": "^5.0.6",
"@types/jest": "^30.0.0",
"@types/node": "^22.19.15",
"@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.3.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.4.6",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.9.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,9 @@
// apps/api/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

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();
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { SpacesModule } from './spaces/spaces.module';
@Module({
imports: [PrismaModule, SpacesModule],
controllers: [AppController],
providers: [AppService],
})
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,34 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const isHttpException = exception instanceof HttpException;
const status = isHttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = isHttpException
? exception.getResponse()
: 'Internal server error';
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}

26
apps/api/src/main.ts Normal file
View File

@ -0,0 +1,26 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
app.useGlobalFilters(new HttpExceptionFilter());
app.enableCors({
origin: ['http://localhost:5173'],
credentials: true,
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@ -0,0 +1,13 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { SpacesService } from './spaces.service';
@Controller('spaces')
export class SpacesController {
constructor(private readonly spacesService: SpacesService) {}
@Get()
findAll() {
return this.spacesService.findAll();
}
}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SpacesController } from './spaces.controller';
import { SpacesService } from './spaces.service';
@Module({
controllers: [SpacesController],
providers: [SpacesService],
})
export class SpacesModule {}

View File

@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { SpaceDTO, SpaceType, BookingStatus } from '@hot-desking/shared';
@Injectable()
export class SpacesService {
findAll(): SpaceDTO[] {
return [
{
id: '1',
name: 'Desk 1',
type: SpaceType.Desk,
x: 10,
y: 20,
status: BookingStatus.Available,
},
];
}
}

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"]
}

25
apps/api/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"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
}
}

1
apps/web/.env Normal file
View File

@ -0,0 +1 @@
VITE_API_BASE_URL="http://localhost:3000"

30
apps/web/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.0",
"lucide-svelte": "^0.300.0",
"postcss": "^8.4.0",
"svelte": "^5.0.0",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.0"
},
"type": "module",
"dependencies": {
"import-meta-resolve": "^4.2.0"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

3
apps/web/src/app.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

12
apps/web/src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,21 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';
export async function apiFetch<T>(
path: string,
options: RequestInit = {},
): Promise<T> {
const res = await fetch(`${API_BASE_URL}${path}`, {
headers: {
'Content-Type': 'application/json',
...(options.headers ?? {}),
},
...options,
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Request failed: ${res.status}`);
}
return res.json() as Promise<T>;
}

View File

@ -0,0 +1,17 @@
<script>
import "../app.css";
</script>
<div class="min-h-screen bg-gray-50">
<nav class="bg-white shadow-sm px-4 py-3 flex justify-between items-center">
<h1 class="text-xl font-bold text-indigo-600">Hot Desking</h1>
<div class="space-x-4">
<a href="/" class="text-gray-600 hover:text-indigo-600">Dashboard</a>
<a href="/login" class="text-gray-600 hover:text-indigo-600">Login</a>
</div>
</nav>
<main class="max-w-7xl mx-auto p-6">
<slot />
</main>
</div>

View File

@ -0,0 +1,55 @@
<script lang="ts">
import { onMount } from 'svelte';
import { apiFetch } from '$lib/api/client';
import type { SpaceDTO } from '@shared/index';
import { MapPin, Monitor as DeskIcon } from 'lucide-svelte';
let spaces: SpaceDTO[] = [];
let error: string | null = null;
onMount(async () => {
try {
spaces = await apiFetch<SpaceDTO[]>('/spaces');
} catch (e: any) {
error = e.message;
}
});
</script>
<div class="space-y-6">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-800">Space Dashboard</h2>
<div class="flex items-center space-x-2 text-sm text-gray-500">
<MapPin size={16} />
<span>Main Office</span>
</div>
</div>
{#if error}
<div class="bg-red-50 border-l-4 border-red-400 p-4 text-red-700">
<p>Error loading spaces: {error}</p>
</div>
{:else if spaces.length === 0}
<p class="text-gray-500 italic">No spaces found or loading...</p>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each spaces as space}
<div class="bg-white p-4 rounded-lg shadow border border-gray-100 flex items-center space-x-4">
<div class="p-3 bg-indigo-50 text-indigo-600 rounded-full">
<DeskIcon size={24} />
</div>
<div>
<h3 class="font-semibold text-gray-900">{space.name}</h3>
<p class="text-sm text-gray-500 capitalize">{space.type}{space.status}</p>
</div>
<div class="ml-auto">
<span class="px-2 py-1 text-xs font-medium rounded-full
{space.status === 'Available' ? 'bg-green-100 text-green-700' : 'bg-orange-100 text-orange-700'}">
{space.status}
</span>
</div>
</div>
{/each}
</div>
{/if}
</div>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { LogIn } from 'lucide-svelte';
</script>
<div class="max-w-md mx-auto mt-20 bg-white p-8 rounded-xl shadow-lg border border-gray-100">
<div class="flex flex-col items-center mb-8">
<div class="p-4 bg-indigo-100 text-indigo-600 rounded-full mb-4">
<LogIn size={32} />
</div>
<h2 class="text-3xl font-bold text-gray-900">Sign In</h2>
<p class="text-gray-500">Access the Hot Desking system</p>
</div>
<form class="space-y-4">
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email Address</label>
<input type="email" id="email" class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="you@company.com" required>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
<input type="password" id="password" class="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" required>
</div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors">
Login
</button>
</form>
</div>

16
apps/web/svelte.config.js Normal file
View File

@ -0,0 +1,16 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
'@shared': '../../shared'
}
}
};
export default config;

View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [],
}

14
apps/web/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

11
apps/web/vite.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
server: {
fs: {
allow: ['../../shared']
}
}
});

30
docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
version: "3.9"
services:
postgres:
image: postgres:16
container_name: hotdesking_postgres
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: hotdesking
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
pgadmin:
image: dpage/pgadmin4:8
container_name: hotdesking_pgadmin
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: admin@local.dev
PGADMIN_DEFAULT_PASSWORD: admin
ports:
- "5051:80"
depends_on:
- postgres
volumes:
postgres_data:

11433
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "hot-desking-app",
"private": true,
"workspaces": [
"apps/*",
"shared"
],
"scripts": {
"dev:web": "npm --workspace apps/web run dev",
"dev:api": "npm --workspace apps/api run start:dev",
"build": "npm run build --workspaces",
"lint": "npm run lint --workspaces"
}
}

82
prompts.md Normal file
View File

@ -0,0 +1,82 @@
Stage 1: The Core Foundation (Structure & Auth)
Goal: Define the database and the hierarchical API.
Agent Prompt:
"Act as a Senior Backend Architect. Design a NestJS API and a PostgreSQL schema using Prisma/TypeORM.
Create a hierarchical data model: Organization -> Building -> Floor -> Space (where Space is an enum: Desk, Meeting Room, Amenity).
Implement RBAC (Role-Based Access Control) for roles: Admin, Supervisor, and Employee.
Provide the NestJS boilerplate for the Spaces module, including a recursive GET endpoint to fetch a full Floor layout.
Generate a SvelteKit login page and a basic dashboard that lists Buildings and Floors based on the user's Org."
Stage 2: Live State & Real-Time Booking
Goal: Implement WebSockets for instant UI updates.
Agent Prompt:
"Act as a Full-Stack Engineer. I need to implement real-time desk availability.
In the NestJS backend, set up a Socket.io Gateway. When a Reservation is created or cancelled, emit a space_status_changed event to a room identified by floorId.
Create the Reservations table in Postgres with fields for startTime, endTime, userId, and spaceId.
On the SvelteKit frontend, create a SpaceCard component that listens to the WebSocket. If a message arrives for its spaceId, it should immediately toggle its visual state (Available/Occupied) without a page reload."
Stage 3: The Visual Floorplan (Drag & Drop)
Goal: Create the interactive map editor.
Agent Prompt:
"Act as a Frontend Expert. I need a SvelteKit interface for an 'Admin Floorplan Editor.'
Create a component where an Admin can upload an image (JPG/PNG) of a floorplan to use as a background.
Implement a 'Toolbox' of Draggable icons (Desk, Toilet, Coffee, Meeting Room).
Use a library like svelte-dnd-action or interact.js to allow placing these icons on the floorplan.
On 'Save,' the component must calculate the percentage-based X,Y coordinates (to ensure responsiveness) and send an array of objects to the NestJS /spaces/batch-update endpoint."
Stage 4: Google Calendar Sync
Goal: Connect the booking system to the outside world.
Agent Prompt:
"Act as an Integration Specialist. I need to sync Meeting Room bookings with Google Calendar in NestJS.
Provide a service that uses googleapis to authenticate via a Service Account.
Write a 'Sync Service' that:
When a user books in-app, creates a GCal event.
Uses a Cron job to poll GCal every 5 minutes for external bookings and updates the local Reservations table.
Ensure that if a Google Event is deleted, the app marks the room as 'Available' locally."
Stage 5: Kiosk Mode (Tablet UI)
Goal: Build the high-visibility, simplified interface.
Agent Prompt:
"Act as a UI/UX Designer and Svelte Developer. Design a 'Kiosk Mode' page.
Route: /kiosk/:spaceId. This page should be 'Full Screen' and optimized for a 10-inch tablet.
Requirements:
Large status indicator (Big Red/Green background).
Display 'Current Meeting' and 'Next Meeting' (fetched from the Google Sync service).
A 'Quick Book' button that allows a user to claim the room for 15, 30, or 60 minutes instantly.
Implement a simple PIN-code overlay if the user tries to exit the kiosk view to the main dashboard."
Suggested Next Step
Since Stage 1 is the bedrock, would you like me to generate the actual Prisma Schema or TypeORM Entities for that Org > Building > Floor > Desk hierarchy right now?

24
shared/dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
export declare enum SpaceType {
Desk = "Desk",
Room = "Room",
Amenity = "Amenity"
}
export declare enum BookingStatus {
Available = "Available",
Occupied = "Occupied",
Reserved = "Reserved",
OutOfService = "OutOfService"
}
export declare enum UserRole {
Admin = "Admin",
Supervisor = "Supervisor",
Employee = "Employee"
}
export interface SpaceDTO {
id: string;
name: string;
type: SpaceType;
x: number;
y: number;
status: BookingStatus;
}

22
shared/dist/index.js vendored Normal file
View File

@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserRole = exports.BookingStatus = exports.SpaceType = void 0;
var SpaceType;
(function (SpaceType) {
SpaceType["Desk"] = "Desk";
SpaceType["Room"] = "Room";
SpaceType["Amenity"] = "Amenity";
})(SpaceType || (exports.SpaceType = SpaceType = {}));
var BookingStatus;
(function (BookingStatus) {
BookingStatus["Available"] = "Available";
BookingStatus["Occupied"] = "Occupied";
BookingStatus["Reserved"] = "Reserved";
BookingStatus["OutOfService"] = "OutOfService";
})(BookingStatus || (exports.BookingStatus = BookingStatus = {}));
var UserRole;
(function (UserRole) {
UserRole["Admin"] = "Admin";
UserRole["Supervisor"] = "Supervisor";
UserRole["Employee"] = "Employee";
})(UserRole || (exports.UserRole = UserRole = {}));

27
shared/index.ts Normal file
View File

@ -0,0 +1,27 @@
export enum SpaceType {
Desk = 'Desk',
Room = 'Room',
Amenity = 'Amenity',
}
export enum BookingStatus {
Available = 'Available',
Occupied = 'Occupied',
Reserved = 'Reserved',
OutOfService = 'OutOfService',
}
export enum UserRole {
Admin = 'Admin',
Supervisor = 'Supervisor',
Employee = 'Employee',
}
export interface SpaceDTO {
id: string;
name: string;
type: SpaceType;
x: number;
y: number;
status: BookingStatus;
}

10
shared/package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "@hot-desking/shared",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"private": true,
"scripts": {
"build": "tsc"
}
}

13
shared/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./",
"declaration": true,
"emitDeclarationOnly": false,
"module": "CommonJS",
"moduleResolution": "node",
"target": "ES2022"
},
"include": ["index.ts"]
}

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
console.log('Happy developing ✨')

15
tsconfig.base.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"baseUrl": ".",
"paths": {
"@shared/*": ["shared/*"]
},
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist",
"rootDir": "."
},
"include": ["src/**/*", "shared/**/*"]
}