mirror of
https://github.com/CarGDev/pomodoro.git
synced 2025-09-18 18:58:27 +00:00
Set up project configuration and base UI components
Initializes the project with necessary configurations, including Replit settings and a .gitignore file. It also introduces foundational UI components for the application, such as buttons, dialogs, and layout elements, likely for the Pomodoro timer application. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 59a5ae27-3c71-459b-b42f-fe14121bf9c3 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3007b6f6-d03b-45e1-9ed1-7ce8de18ea24/59a5ae27-3c71-459b-b42f-fe14121bf9c3/Uupe4F4
This commit is contained in:
71
server/index.ts
Normal file
71
server/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
||||
|
||||
const originalResJson = res.json;
|
||||
res.json = function (bodyJson, ...args) {
|
||||
capturedJsonResponse = bodyJson;
|
||||
return originalResJson.apply(res, [bodyJson, ...args]);
|
||||
};
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
if (path.startsWith("/api")) {
|
||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||
if (capturedJsonResponse) {
|
||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||
}
|
||||
|
||||
if (logLine.length > 80) {
|
||||
logLine = logLine.slice(0, 79) + "…";
|
||||
}
|
||||
|
||||
log(logLine);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const server = await registerRoutes(app);
|
||||
|
||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
const message = err.message || "Internal Server Error";
|
||||
|
||||
res.status(status).json({ message });
|
||||
throw err;
|
||||
});
|
||||
|
||||
// importantly only setup vite in development and after
|
||||
// setting up all the other routes so the catch-all route
|
||||
// doesn't interfere with the other routes
|
||||
if (app.get("env") === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// ALWAYS serve the app on the port specified in the environment variable PORT
|
||||
// Other ports are firewalled. Default to 5000 if not specified.
|
||||
// this serves both the API and the client.
|
||||
// It is the only port that is not firewalled.
|
||||
const port = parseInt(process.env.PORT || '5000', 10);
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
reusePort: true,
|
||||
}, () => {
|
||||
log(`serving on port ${port}`);
|
||||
});
|
||||
})();
|
171
server/routes.ts
Normal file
171
server/routes.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { insertSessionSchema } from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Create session
|
||||
app.post("/api/sessions", async (req, res) => {
|
||||
try {
|
||||
const sessionData = insertSessionSchema.parse(req.body);
|
||||
const session = await storage.createSession(sessionData);
|
||||
|
||||
// Update device activity
|
||||
await storage.updateDeviceProfileActivity(sessionData.deviceId);
|
||||
|
||||
res.status(201).json({ ok: true, id: session.id });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: "Invalid session data", errors: error.errors });
|
||||
}
|
||||
res.status(500).json({ message: "Failed to create session" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get suggestions for device
|
||||
app.get("/api/suggestions", async (req, res) => {
|
||||
try {
|
||||
const deviceId = req.query.deviceId as string;
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ message: "Device ID is required" });
|
||||
}
|
||||
|
||||
// Get recent sessions (last 7 days)
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
const recentSessions = await storage.getSessionsInDateRange(deviceId, sevenDaysAgo, new Date());
|
||||
|
||||
const suggestions = generateSuggestions(recentSessions);
|
||||
res.json({ suggestions });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to get suggestions" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get stats summary
|
||||
app.get("/api/stats/summary", async (req, res) => {
|
||||
try {
|
||||
const deviceId = req.query.deviceId as string;
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ message: "Device ID is required" });
|
||||
}
|
||||
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const last7dSessions = await storage.getSessionsInDateRange(deviceId, sevenDaysAgo, new Date());
|
||||
const last30dSessions = await storage.getSessionsInDateRange(deviceId, thirtyDaysAgo, new Date());
|
||||
|
||||
const last7d = calculateStats(last7dSessions.filter(s => s.type === 'focus'));
|
||||
const last30d = calculateStats(last30dSessions.filter(s => s.type === 'focus'));
|
||||
|
||||
res.json({ last7d, last30d });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to get stats" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
function generateSuggestions(sessions: any[]) {
|
||||
const focusSessions = sessions.filter(s => s.type === 'focus');
|
||||
|
||||
if (focusSessions.length === 0) {
|
||||
return [
|
||||
{ minutes: 25, reason: "Classic Pomodoro technique - great for starting out." },
|
||||
{ minutes: 15, reason: "Short sprints help build focus habits." },
|
||||
{ minutes: 40, reason: "Extended sessions for deep work." }
|
||||
];
|
||||
}
|
||||
|
||||
const completedSessions = focusSessions.filter(s => s.completed);
|
||||
const completionRate = completedSessions.length / focusSessions.length;
|
||||
|
||||
// Analyze successful durations
|
||||
const successfulDurations = completedSessions.map(s => s.intendedMinutes);
|
||||
const avgSuccessfulDuration = successfulDurations.reduce((a, b) => a + b, 0) / successfulDurations.length;
|
||||
|
||||
// Analyze time of day patterns
|
||||
const now = new Date();
|
||||
const currentHour = now.getHours();
|
||||
const timeOfDaySuccess = getTimeOfDaySuccess(completedSessions, currentHour);
|
||||
|
||||
const suggestions = [];
|
||||
|
||||
// Primary suggestion based on completion rate
|
||||
if (completionRate >= 0.8) {
|
||||
suggestions.push({
|
||||
minutes: Math.round(avgSuccessfulDuration),
|
||||
reason: `Your 7-day completion rate is ${Math.round(completionRate * 100)}% at ${Math.round(avgSuccessfulDuration)}m.`
|
||||
});
|
||||
} else {
|
||||
suggestions.push({
|
||||
minutes: 25,
|
||||
reason: "Classic 25min sessions have proven effective for most users."
|
||||
});
|
||||
}
|
||||
|
||||
// Time-based suggestion
|
||||
if (timeOfDaySuccess.avgDuration > 0) {
|
||||
suggestions.push({
|
||||
minutes: Math.round(timeOfDaySuccess.avgDuration),
|
||||
reason: `${getTimeOfDayLabel(currentHour)}: best focus avg ${Math.round(timeOfDaySuccess.avgDuration - 3)}–${Math.round(timeOfDaySuccess.avgDuration + 3)}m.`
|
||||
});
|
||||
}
|
||||
|
||||
// Interruption-based suggestion
|
||||
const avgInterruptions = focusSessions.reduce((sum, s) => sum + s.interruptions, 0) / focusSessions.length;
|
||||
if (avgInterruptions > 1) {
|
||||
suggestions.push({
|
||||
minutes: 15,
|
||||
reason: "High interruption periods benefit from short sprints."
|
||||
});
|
||||
} else {
|
||||
suggestions.push({
|
||||
minutes: 40,
|
||||
reason: "Low interruption environment - try longer deep work sessions."
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions.slice(0, 3);
|
||||
}
|
||||
|
||||
function calculateStats(focusSessions: any[]) {
|
||||
const completed = focusSessions.filter(s => s.completed);
|
||||
const completionRate = focusSessions.length > 0 ? completed.length / focusSessions.length : 0;
|
||||
const avgFocusMin = completed.length > 0
|
||||
? completed.reduce((sum, s) => sum + s.intendedMinutes, 0) / completed.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
sessions: focusSessions.length,
|
||||
completionRate: Math.round(completionRate * 100) / 100,
|
||||
avgFocusMin: Math.round(avgFocusMin)
|
||||
};
|
||||
}
|
||||
|
||||
function getTimeOfDaySuccess(sessions: any[], currentHour: number) {
|
||||
const timeRangeSessions = sessions.filter(s => {
|
||||
const sessionHour = new Date(s.startedAt).getHours();
|
||||
return Math.abs(sessionHour - currentHour) <= 2;
|
||||
});
|
||||
|
||||
if (timeRangeSessions.length === 0) {
|
||||
return { avgDuration: 0 };
|
||||
}
|
||||
|
||||
const avgDuration = timeRangeSessions.reduce((sum, s) => sum + s.intendedMinutes, 0) / timeRangeSessions.length;
|
||||
return { avgDuration };
|
||||
}
|
||||
|
||||
function getTimeOfDayLabel(hour: number) {
|
||||
if (hour >= 6 && hour < 12) return "Mornings";
|
||||
if (hour >= 12 && hour < 17) return "Afternoons";
|
||||
if (hour >= 17 && hour < 21) return "Evenings";
|
||||
return "Late hours";
|
||||
}
|
76
server/storage.ts
Normal file
76
server/storage.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { type Session, type InsertSession, type DeviceProfile, type InsertDeviceProfile } from "@shared/schema";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export interface IStorage {
|
||||
// Sessions
|
||||
createSession(session: InsertSession): Promise<Session>;
|
||||
getSessionsByDeviceId(deviceId: string, limit?: number): Promise<Session[]>;
|
||||
getSessionsInDateRange(deviceId: string, startDate: Date, endDate: Date): Promise<Session[]>;
|
||||
|
||||
// Device Profiles
|
||||
createDeviceProfile(profile: InsertDeviceProfile): Promise<DeviceProfile>;
|
||||
getDeviceProfile(deviceId: string): Promise<DeviceProfile | undefined>;
|
||||
updateDeviceProfileActivity(deviceId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private deviceProfiles: Map<string, DeviceProfile> = new Map();
|
||||
|
||||
async createSession(insertSession: InsertSession): Promise<Session> {
|
||||
const id = randomUUID();
|
||||
const session: Session = {
|
||||
...insertSession,
|
||||
id,
|
||||
};
|
||||
this.sessions.set(id, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async getSessionsByDeviceId(deviceId: string, limit = 50): Promise<Session[]> {
|
||||
const sessions = Array.from(this.sessions.values())
|
||||
.filter(session => session.deviceId === deviceId)
|
||||
.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
|
||||
.slice(0, limit);
|
||||
return sessions;
|
||||
}
|
||||
|
||||
async getSessionsInDateRange(deviceId: string, startDate: Date, endDate: Date): Promise<Session[]> {
|
||||
const sessions = Array.from(this.sessions.values())
|
||||
.filter(session => {
|
||||
const sessionDate = new Date(session.startedAt);
|
||||
return session.deviceId === deviceId &&
|
||||
sessionDate >= startDate &&
|
||||
sessionDate <= endDate;
|
||||
})
|
||||
.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
||||
return sessions;
|
||||
}
|
||||
|
||||
async createDeviceProfile(insertProfile: InsertDeviceProfile): Promise<DeviceProfile> {
|
||||
const id = randomUUID();
|
||||
const now = new Date();
|
||||
const profile: DeviceProfile = {
|
||||
...insertProfile,
|
||||
id,
|
||||
createdAt: now,
|
||||
lastActiveAt: now,
|
||||
};
|
||||
this.deviceProfiles.set(insertProfile.deviceId, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
async getDeviceProfile(deviceId: string): Promise<DeviceProfile | undefined> {
|
||||
return this.deviceProfiles.get(deviceId);
|
||||
}
|
||||
|
||||
async updateDeviceProfileActivity(deviceId: string): Promise<void> {
|
||||
const profile = this.deviceProfiles.get(deviceId);
|
||||
if (profile) {
|
||||
profile.lastActiveAt = new Date();
|
||||
this.deviceProfiles.set(deviceId, profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new MemStorage();
|
85
server/vite.ts
Normal file
85
server/vite.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createServer as createViteServer, createLogger } from "vite";
|
||||
import { type Server } from "http";
|
||||
import viteConfig from "../vite.config";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const viteLogger = createLogger();
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
console.log(`${formattedTime} [${source}] ${message}`);
|
||||
}
|
||||
|
||||
export async function setupVite(app: Express, server: Server) {
|
||||
const serverOptions = {
|
||||
middlewareMode: true,
|
||||
hmr: { server },
|
||||
allowedHosts: true as const,
|
||||
};
|
||||
|
||||
const vite = await createViteServer({
|
||||
...viteConfig,
|
||||
configFile: false,
|
||||
customLogger: {
|
||||
...viteLogger,
|
||||
error: (msg, options) => {
|
||||
viteLogger.error(msg, options);
|
||||
process.exit(1);
|
||||
},
|
||||
},
|
||||
server: serverOptions,
|
||||
appType: "custom",
|
||||
});
|
||||
|
||||
app.use(vite.middlewares);
|
||||
app.use("*", async (req, res, next) => {
|
||||
const url = req.originalUrl;
|
||||
|
||||
try {
|
||||
const clientTemplate = path.resolve(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"client",
|
||||
"index.html",
|
||||
);
|
||||
|
||||
// always reload the index.html file from disk incase it changes
|
||||
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
||||
template = template.replace(
|
||||
`src="/src/main.tsx"`,
|
||||
`src="/src/main.tsx?v=${nanoid()}"`,
|
||||
);
|
||||
const page = await vite.transformIndexHtml(url, template);
|
||||
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
||||
} catch (e) {
|
||||
vite.ssrFixStacktrace(e as Error);
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath = path.resolve(import.meta.dirname, "public");
|
||||
|
||||
if (!fs.existsSync(distPath)) {
|
||||
throw new Error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`,
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user