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
121 changes: 121 additions & 0 deletions src/app/api/daily-note/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { NextResponse ,NextRequest} from "next/server";
import { supabaseAdmin } from "@/lib/supabase";
import { getToken } from "next-auth/jwt";

export async function GET(req: NextRequest){

try{
const token = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
console.log(token)

const userId = token?.githubId;

if(!userId){
return NextResponse.json(
{error: `Unauthorized`},
{status: 400}
);
}

const today = new Date();
const todayDate = today.toISOString().split("T")[0];

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() -1);

const yesterdayDate = yesterday.toISOString().split("T")[0];

const {data : todayData} = await supabaseAdmin
.from("daily_notes")
.select("*")
.eq("user_id",userId)
.eq("date",todayDate)
.single();

const {data : yesterdayData} = await supabaseAdmin
.from("daily_notes")
.select("*")
.eq("user_id",userId)
.eq("date",yesterdayDate)
.single();
console.log(todayData,yesterdayData);
return NextResponse.json({
todayNote: todayData?.note || "",
yesterdayNote: yesterdayData?.note || "",
});

}catch(error){
return NextResponse.json(
{error: `Something went wrong `},
{status: 500}
);

}
}

export async function POST(req: NextRequest) {
try{

const token = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
console.log(token)

const userId = token?.githubId;
const body = await req.json();
console.log(body);
const { note} = body;
if(!userId){
console.log("no user id");
return NextResponse.json(
{error: "User id is requied "},
{status: 400}
);
}
if(!note || !note.trim()){
console.log("no note ");
return NextResponse.json(
{error: "Note cannot be empty"},
{status: 400}
);
}
if(note.length >280){
console.log("max len ");
return NextResponse.json(
{error: "Maximum 280 characters allowed"},
{status: 400}
);
}
const today = new Date().toISOString().split("T")[0];
const {data, error} = await supabaseAdmin
.from("daily_notes")
.upsert({
user_id: userId,
date: today,
note: note.trim(),
},{
onConflict:"user_id,date"
})
.select()
.single();
if(error){
console.log(error.message);
return NextResponse.json(
{error: error.message},
{status:500}
);
}
console.log(data);
return NextResponse.json(data);
}catch(error){
console.log(error);
return NextResponse.json(
{error: "Something went wrong"},
{status:500}
);
}
}
22 changes: 22 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ const PRReviewTrendChart = dynamic(
() => import("@/components/PRReviewTrendChart"),
{ ssr: false, loading: () => <SkeletonCard /> },
);
import WeeklySummaryCard from "@/components/WeeklySummaryCard";
import { AIMentorWidget } from "@/components/AIMentorWidget";
import ExportButton from "@/components/ExportButton";
import Link from "next/link";
import PersonalRecords from "@/components/PersonalRecords";
import LocalCodingTime from "@/components/LocalCodingTime";
import CodingTimeWidget from "@/components/CodingTimeWidget";
import RecentActivity from "@/components/RecentActivity";
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import DailyNoteWidget from "@/components/DailyNoteWidget";
import DashboardSSEProvider from "@/components/DashboardSSEProvider";

export default async function DashboardPage() {
const session = await getServerSession(authOptions);
Expand Down Expand Up @@ -170,6 +183,15 @@ export default async function DashboardPage() {
</LazyWidget>
</div>

<div>
<DailyNoteWidget/>
<StreakTracker />
<LocalCodingTime />
<div className="mt-6">
<CodingTimeCard />
</div>
{/* Row 2: PR metrics, community metrics, PR breakdown & Time Chart */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{/* ── Row 2: PR metrics + Community metrics ── */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<PRMetrics />
Expand Down
1 change: 0 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ async function fetchRepoStats(): Promise<RepoStats> {

export default async function HomePage() {
const session = await getServerSession(authOptions);

if (session) {
redirect("/dashboard");
}
Expand Down
126 changes: 126 additions & 0 deletions src/components/DailyNoteWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import { useEffect, useState } from "react";

export default function DailyNoteWidget(){

const [loading,setLoading] = useState(false);
const [note,setNote] = useState("");
const [yesterdayNote, setYesterdayNote] = useState("");
const [showYesterday,setShowYesterday] = useState(false);

useEffect(()=>{

const fetchNotes = async ()=>{

try{
setLoading(true);

const response = await fetch("/api/daily-note");

const data = await response.json();
setNote(data.todayNote||"");
setYesterdayNote(data.yesterdayNote ||"" );

}catch(error){
console.error("Failed to fetch notes");
}finally{
setLoading(false);
}

};
fetchNotes();
},[]);


// auto save with debounce
const debounceFunction = async()=>{

if(!note.trim()) return ;

try{
await fetch("/api/daily-note",{
method:"POST",
headers:{
"Content-Type": "application/json",
},
body: JSON.stringify({
note,
}),
});
}catch(error){
console.error("Failed to save note");
}
}
useEffect(()=>{

const timeout= setTimeout(()=>{
debounceFunction();
},500);
return ()=>clearTimeout(timeout);
},[note]);


if (loading) {
return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<div className="h-5 w-40 bg-[var(--card-muted)] rounded animate-pulse mb-4" />
<div className="space-y-2">
<div className="h-4 bg-[var(--card-muted)] rounded animate-pulse w-3/4" />
<div className="h-4 bg-[var(--card-muted)] rounded animate-pulse w-1/2" />
</div>
</div>
);
}

return(

<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6 shadow-sm">
<div className="m-3 w-40 rounded mb-4 ml-0" >
<h2 className="text-lg font-semibold text-[var(--card-foreground)] ">
Today's Plan` `
</h2>

<p className="text-sm text-[var(--muted-foreground)]">Plan your coding session.</p>
</div>
<div className="space-y-2">


<textarea value={note}
onChange={(e)=>{
if(e.target.value.length <= 280){
setNote(e.target.value);
}
}}
onBlur={debounceFunction}
placeholder="What will you code today?"
rows={3}
className=" w-full resize-none rounded-md border border-gray-300 p-2
focus:outline-none transition focus:border-gray-500"
/>

<div className="mt-2 flex items-center justify-between">

<p className="text-xs text-gray-400">
Auto-saves automatically
</p>
<p className="text-xs text-gray-500">
{note.length}/280
</p>
</div>

<div className="mt-4 rounded-md bg-gray-50 p-3">
<p className="text-xs text-gray-500 cursor-pointer" onClick={()=> setShowYesterday((prev)=> !prev)} aria-expanded={showYesterday} >
Yesterday you planned to:
</p>
{showYesterday &&
<p className="mt-1 text-sm text-gray-700">
{yesterdayNote || "No plan form yesterday"}
</p>
}

</div>
</div>
</div>
)
}
10 changes: 10 additions & 0 deletions supabase/migrations/20260515000002_add_daily_notes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

create table if not exists daily_notes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id text not null,
date date not null,
note text,
created_at timestamptz default now(),

UNIQUE(user_id, date)
);
10 changes: 10 additions & 0 deletions supabase/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ create index if not exists idx_ai_insights_type on ai_insights(insight_type);
create unique index if not exists idx_ai_insights_user_type
on ai_insights(user_id, insight_type);

-- daily_notes schema--
create table if not exists daily_notes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id text not null,
date date not null,
note text,
created_at timestamptz default now(),

UNIQUE(user_id, date)
);
create table if not exists user_github_achievements (
user_id text primary key references users(id) on delete cascade,
github_login text not null,
Expand Down
Loading