Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions backend/src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { timestamp, pgTable, varchar, integer, unique, pgEnum } from "drizzle-orm/pg-core";
import { timestamp, pgTable, varchar, integer, unique, pgEnum, serial, date } from "drizzle-orm/pg-core";

// export const statusEnum = pgEnum("status", ["pending", "completed"]);

export const users = pgTable("users", {
id: varchar("id", { length: 255 }).primaryKey(),
email: varchar("email", { length: 255 }).notNull(),
name: varchar("name", { length: 255 }),
createdAt: timestamp("created_at"),
createdAt: timestamp("created_at", { withTimezone: true }),
});

export const categories = pgTable("categories", {
Expand All @@ -22,7 +22,7 @@ export const tasksList = pgTable("tasks_lists", {
userId: varchar("user_id").references(() => users.id, {onDelete: 'cascade'}).notNull(),
title: varchar("title", { length: 255 }).notNull(),
categoryId: integer("category_id").references(() => categories.id, {onDelete: 'set null'}),
createdAt: timestamp("created_at").defaultNow()
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow()
});

export const statusEnum = pgEnum("status", ['incomplete', 'in_progress', 'completed']);
Expand All @@ -33,4 +33,15 @@ export const tasks = pgTable("tasks", {
title: varchar("title", { length: 255 }).notNull(),
status: statusEnum(),
order: integer("order").notNull().default(0)
});

export const productivityTimer = pgTable("productivity_timer", {
id: serial("id").primaryKey(),
userId: varchar("user_id").references(() => users.id, { onDelete: 'cascade'}).notNull(),
startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
endedAt: timestamp("ended_at", { withTimezone: true }).notNull(),
duration: integer("duration"),
date: date("date").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow()
});
2 changes: 2 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { swaggerUI, SwaggerUI } from '@hono/swagger-ui';

import { swaggerSpec } from './lib/swagger';
import analyticsRouter from './routes/analyticsRoutes';
import pRouter from './routes/productivityRoutes';

const app = new Hono()

Expand Down Expand Up @@ -55,6 +56,7 @@ app.route("/user", userRouter);
app.route("/task", tasksRouter);
app.route("/category", categoryRouter);
app.route("/analytics", analyticsRouter);
app.route("/prod", pRouter); // pRouter -> productivityRouter (Didn't want to write productivityRouter over and over again)

const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_GEMINI_API_KEY });

Expand Down
134 changes: 134 additions & 0 deletions backend/src/routes/productivityRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Hono } from "hono";
import { requireAuth } from "../middleware/requireAuth";
import { productivityTimer } from "../db/schema";
import { db } from "../db/db";
import { eq, and } from "drizzle-orm";

const pRouter = new Hono();

type AddBodyType = {
// checked
startTime: number;
endTime: number;
timeZone?: string;
};

function isoDateInTimeZone(d: Date, timeZone: string | undefined) {
const dtf = new Intl.DateTimeFormat('en-CA', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
return dtf.format(d);
}

pRouter.post("/add", requireAuth, async (c) => {
try {
const { userId } = c.get("authData");
const { startTime, endTime, timeZone }: AddBodyType = await c.req.json();

if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) {
return c.json({ message: "Start and end must be finite!" }, 400);
}

const durationSec = (endTime - startTime) / 1000;

const startDate = new Date(startTime);
const endDate = new Date(endTime);

const startDateOnly = isoDateInTimeZone(startDate, timeZone);
const endDateOnly = isoDateInTimeZone(endDate, timeZone);

let dateOnly: string | undefined;

if(Number.parseInt(startDateOnly.substring(8)) == Number.parseInt(endDateOnly.substring(8))) {
dateOnly = startDateOnly;
} else if (Number.parseInt(startDateOnly.substring(8)) < Number.parseInt(endDateOnly.substring(8))) {
dateOnly = endDateOnly;
} else {
return c.json({ success: false, message: "End date cannot be smaller than start date" }, 400);
}

// checking for existing data
const dates = await db
.select()
.from(productivityTimer)
.where(
and(
eq(productivityTimer.date, dateOnly),
eq(productivityTimer.startedAt, startDate),
eq(productivityTimer.endedAt, endDate),
eq(productivityTimer.userId, userId)
)
);
console.log("Existing Dates =", dates);

if (dates.length == 0) {
// If there is no existing entry
const res = await db.insert(productivityTimer).values({
userId: userId,
startedAt: startDate,
endedAt: endDate,
duration: Math.floor(durationSec),
date: dateOnly,
});

console.log("Result = " + res);

return c.json(
{ success: true, message: "Time has been added", res },
200
);
} else {
console.error("The given time already exists!");
return c.json({ message: "Already data present" }, 400);
}
} catch (err) {
console.error("An error occured while adding time", err);
return c.json({ message: "Internal Server Error" }, 500);
}
});

type Group = {
date: string;
totalDuration: number;
}

type Row = {
date: string;
duration: number | null;
}

pRouter.get("/get_time", requireAuth, async (c) => {
try {
const { userId } = c.get("authData");
const data: Row[] = await db
.select({
duration: productivityTimer.duration,
date: productivityTimer.date,
})
.from(productivityTimer)
.where(eq(productivityTimer.userId, userId));

console.log("Data is =", data);

const dataFixed = data.reduce<Group[]>((acc, { date, duration }) => {

const amt = duration ?? 0;
const found = acc.find((ele) => ele.date === date);
if(found) {
found.totalDuration += amt;
} else {
acc.push({ date, totalDuration: amt });
}
return acc;
}, []);
return c.json({ success: true, dataFixed }, 200);
} catch (err) {
console.error("An error occured while fetching durations per day =", err);
return c.json({ message: "Internal Server Error" }, 500);
}
});

export default pRouter;
2 changes: 1 addition & 1 deletion backend/src/routes/taskRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ tasksRouter.delete('/delete_task/:id', requireAuth, async (c) => {
* content:
* application/json:
* schema:
* type: object*
* type: object
*/
tasksRouter.post('/add_list', requireAuth, async (c) => {
try {
Expand Down
71 changes: 71 additions & 0 deletions frontend/src/components/dashboard/global_components/miniTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useTimer } from "@/context/useTimer";
import { useMemo } from "react";
import { TimerTypes } from "@/helpers/helpers";
import { Square, Pause, Play } from "lucide-react";

export default function MiniTimer() {
const { status, workTime, breakTime, timerType, start, pause, stop } =
useTimer();

const active = status !== "idle";
const paused = status === "paused";

const handleStopPause = (type: "stop" | "pause") => {
if (type === "stop") stop();
else pause();
};

const display = useMemo(() => {
const t = Math.max(
0,
Math.floor(timerType === TimerTypes.WORK ? workTime : breakTime)
);
const minutes = Math.floor((t % 3600) / 60);
const seconds = t % 60;

const mm = String(minutes).padStart(2, "0");
const ss = String(seconds).padStart(2, "0");

return (
<p>
{mm}:{ss}
</p>
);
}, [workTime, breakTime, timerType]);

// if timer is stopped do not show anything
if (!active) {
return;
}

return (
<div className="bg-violet-100 rounded-full px-5 py-2 flex gap-4 items-center">
<section className="flex gap-1">
<div className="text-sm text-gray-800">{timerType === TimerTypes.WORK ? "📔Work Time" : timerType === TimerTypes.BREAK ? "😪Break Time" : "😴Long Break Time"}</div>
<div>{display}</div>
</section>
<section className="flex gap-2">
<span
title="Stop Timer"
className="cursor-pointer"
onClick={() => handleStopPause("stop")}
>
<Square strokeWidth={0} fill="#FF4C33" size={17} />
</span>
{paused ? (
<span title="Reume Timer" className="cursor-pointer" onClick={start}>
<Play fill="#00EB05" strokeWidth={0} size={17} />
</span>
) : (
<span
title="Pause Timer"
className="cursor-pointer"
onClick={() => handleStopPause("pause")}
>
<Pause fill="#C9B900" strokeWidth={0} size={17} />
</span>
)}
</section>
</div>
);
}
Loading