feat: use gpt to create project, readme

This commit is contained in:
2025-09-30 15:39:32 +08:00
parent e23d5e829f
commit 99a97139df
50 changed files with 7476 additions and 0 deletions

2
server/.env.example Normal file
View File

@@ -0,0 +1,2 @@
MONGODB_URI=mongodb://localhost:27017/travel-journal
PORT=4000

13
server/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,13 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
env: {
node: true
}
};

4
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.env
coverage

33
server/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "traveling-around-the-world-server",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js",
"lint": "eslint src --ext .ts"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"helmet": "^7.1.0",
"mongoose": "^8.3.3",
"morgan": "^1.10.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.7",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.2"
}
}

27
server/src/app.ts Normal file
View File

@@ -0,0 +1,27 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { visitRouter } from './modules/visits/visit.routes';
export function createApp() {
const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
app.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});
app.use('/api/visits', visitRouter);
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
res.status(500).json({ message: 'Internal server error' });
});
return app;
}

15
server/src/config/env.ts Normal file
View File

@@ -0,0 +1,15 @@
import 'dotenv/config';
const get = (key: string, fallback?: string): string => {
const value = process.env[key] ?? fallback;
if (!value) {
throw new Error(`Missing environment variable: ${key}`);
}
return value;
};
export const env = {
nodeEnv: process.env.NODE_ENV ?? 'development',
port: parseInt(process.env.PORT ?? '4000', 10),
mongoUri: get('MONGODB_URI', 'mongodb://localhost:27017/travel-journal')
};

18
server/src/index.ts Normal file
View File

@@ -0,0 +1,18 @@
import { createApp } from './app';
import { connectMongo } from './infra/db/mongo';
import { env } from './config/env';
async function bootstrap() {
try {
await connectMongo();
const app = createApp();
app.listen(env.port, () => {
console.log(`API server ready at http://localhost:${env.port}`);
});
} catch (error) {
console.error('Failed to initialize server', error);
process.exit(1);
}
}
bootstrap();

View File

@@ -0,0 +1,10 @@
import mongoose from 'mongoose';
import { env } from '../../config/env';
export async function connectMongo(): Promise<void> {
await mongoose.connect(env.mongoUri);
}
export function disconnectMongo(): Promise<void> {
return mongoose.disconnect();
}

View File

@@ -0,0 +1,49 @@
import type { Request, Response } from 'express';
import {
createVisit,
deleteVisit,
getVisit,
listVisits,
updateVisit
} from './visit.service';
import { visitInputSchema, visitUpdateSchema } from './visit.types';
import { toVisitDto } from './visit.mapper';
export async function listVisitsHandler(_req: Request, res: Response) {
const visits = await listVisits();
res.json(visits.map(toVisitDto));
}
export async function createVisitHandler(req: Request, res: Response) {
const parsed = visitInputSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
const visit = await createVisit(parsed.data);
res.status(201).json(toVisitDto(visit));
}
export async function getVisitHandler(req: Request, res: Response) {
const visit = await getVisit(req.params.id);
if (!visit) {
return res.status(404).json({ message: 'Visit not found' });
}
res.json(toVisitDto(visit));
}
export async function updateVisitHandler(req: Request, res: Response) {
const parsed = visitUpdateSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
const visit = await updateVisit(req.params.id, parsed.data);
if (!visit) {
return res.status(404).json({ message: 'Visit not found' });
}
res.json(toVisitDto(visit));
}
export async function deleteVisitHandler(req: Request, res: Response) {
await deleteVisit(req.params.id);
res.status(204).send();
}

View File

@@ -0,0 +1,24 @@
import type { VisitDoc } from './visit.model';
import type { VisitDto } from '../../../shared/visit';
export function toVisitDto(doc: VisitDoc): VisitDto {
const json = doc.toJSON();
return {
id: json.id,
location: {
country: json.location.country,
city: json.location.city,
lat: json.location.lat,
lng: json.location.lng
},
date: {
start: new Date(json.date.start).toISOString(),
end: json.date.end ? new Date(json.date.end).toISOString() : undefined
},
notes: json.notes,
tags: json.tags,
photos: json.photos,
createdAt: new Date(json.createdAt).toISOString(),
updatedAt: new Date(json.updatedAt).toISOString()
};
}

View File

@@ -0,0 +1,50 @@
import { Schema, model, type Document } from 'mongoose';
export interface VisitDoc extends Document {
location: {
country: string;
city?: string;
lat: number;
lng: number;
};
date: {
start: Date;
end?: Date;
};
notes?: string;
tags?: string[];
photos?: string[];
createdAt: Date;
updatedAt: Date;
}
const visitSchema = new Schema<VisitDoc>(
{
location: {
country: { type: String, required: true },
city: { type: String },
lat: { type: Number, required: true },
lng: { type: Number, required: true }
},
date: {
start: { type: Date, required: true },
end: { type: Date }
},
notes: { type: String },
tags: [{ type: String }],
photos: [{ type: String }]
},
{ timestamps: true }
);
visitSchema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: (_doc, ret) => {
ret.id = ret._id;
delete ret._id;
return ret;
}
});
export const VisitModel = model<VisitDoc>('Visit', visitSchema);

View File

@@ -0,0 +1,16 @@
import { Router } from 'express';
import {
createVisitHandler,
deleteVisitHandler,
getVisitHandler,
listVisitsHandler,
updateVisitHandler
} from './visit.controller';
export const visitRouter = Router();
visitRouter.get('/', listVisitsHandler);
visitRouter.post('/', createVisitHandler);
visitRouter.get('/:id', getVisitHandler);
visitRouter.put('/:id', updateVisitHandler);
visitRouter.delete('/:id', deleteVisitHandler);

View File

@@ -0,0 +1,23 @@
import type { FilterQuery } from 'mongoose';
import { VisitModel, type VisitDoc } from './visit.model';
import type { VisitInput, VisitUpdateInput } from './visit.types';
export async function listVisits(filter: FilterQuery<VisitDoc> = {}): Promise<VisitDoc[]> {
return VisitModel.find(filter).sort({ 'date.start': -1 }).exec();
}
export async function getVisit(id: string): Promise<VisitDoc | null> {
return VisitModel.findById(id).exec();
}
export async function createVisit(data: VisitInput): Promise<VisitDoc> {
return VisitModel.create(data);
}
export async function updateVisit(id: string, data: VisitUpdateInput): Promise<VisitDoc | null> {
return VisitModel.findByIdAndUpdate(id, data, { new: true }).exec();
}
export async function deleteVisit(id: string): Promise<void> {
await VisitModel.findByIdAndDelete(id).exec();
}

View File

@@ -0,0 +1,26 @@
import { z } from 'zod';
const dateSchema = z.coerce.date();
export const visitSchema = z.object({
location: z.object({
country: z.string().min(1),
city: z.string().optional(),
lat: z.number(),
lng: z.number()
}),
date: z.object({
start: dateSchema,
end: dateSchema.optional()
}),
notes: z.string().optional(),
tags: z.array(z.string()).optional(),
photos: z.array(z.string().url()).optional()
});
export const visitInputSchema = visitSchema;
export const visitUpdateSchema = visitSchema.partial();
export type VisitInput = z.infer<typeof visitInputSchema>;
export type VisitUpdateInput = z.infer<typeof visitUpdateSchema>;

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": false,
"noEmit": false
}
}

17
server/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}