feat: use gpt to create project, readme
This commit is contained in:
27
server/src/app.ts
Normal file
27
server/src/app.ts
Normal 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
15
server/src/config/env.ts
Normal 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
18
server/src/index.ts
Normal 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();
|
10
server/src/infra/db/mongo.ts
Normal file
10
server/src/infra/db/mongo.ts
Normal 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();
|
||||
}
|
49
server/src/modules/visits/visit.controller.ts
Normal file
49
server/src/modules/visits/visit.controller.ts
Normal 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();
|
||||
}
|
24
server/src/modules/visits/visit.mapper.ts
Normal file
24
server/src/modules/visits/visit.mapper.ts
Normal 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()
|
||||
};
|
||||
}
|
50
server/src/modules/visits/visit.model.ts
Normal file
50
server/src/modules/visits/visit.model.ts
Normal 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);
|
16
server/src/modules/visits/visit.routes.ts
Normal file
16
server/src/modules/visits/visit.routes.ts
Normal 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);
|
23
server/src/modules/visits/visit.service.ts
Normal file
23
server/src/modules/visits/visit.service.ts
Normal 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();
|
||||
}
|
26
server/src/modules/visits/visit.types.ts
Normal file
26
server/src/modules/visits/visit.types.ts
Normal 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>;
|
Reference in New Issue
Block a user