Skip to content
Merged
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
25 changes: 23 additions & 2 deletions frontend/src/app/(dashboard)/user/overview/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
"use client";

import React from "react";
import React, { useState, useEffect } from "react";
import ActiveGroupsWidget from "./components/ActiveGroupsWidget";
import ContributorProfileCard from "./components/ContributorProfileCard";
import { UserActivityTimeline } from "@/components/UserActivityTimeline";
import { motion } from "framer-motion";
import { LayoutDashboard } from "lucide-react";
import { getPublicKey } from "@/hooks/stellar-wallets-kit";

export default function OverviewPage() {
const [walletAddress, setWalletAddress] = useState<string | null>(null);

useEffect(() => {
let cancelled = false;

async function checkWallet() {
const key = await getPublicKey();
if (!cancelled) {
setWalletAddress(key ?? "demo-user");
}
}

void checkWallet();
return () => {
cancelled = true;
};
}, []);

return (
<div className="w-full">
{/* Page Header */}
Expand All @@ -24,14 +44,15 @@ export default function OverviewPage() {
Overview
</h1>
<p className="text-[#5A6578] text-xs sm:text-sm font-medium mt-0.5">
Welcome back — here&apos;s a snapshot of your groups
Welcome back — here&apos;s a snapshot of your groups & activities
</p>
</div>
</motion.div>

{/* Widgets Grid */}
<div className="grid grid-cols-1 lg:grid-cols-1 gap-6 sm:gap-8">
<ContributorProfileCard />
<UserActivityTimeline userId={walletAddress} />
<ActiveGroupsWidget />
</div>
</div>
Expand Down
141 changes: 141 additions & 0 deletions frontend/src/app/api/activities/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { buildNoStoreJson } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import {
listUserActivities,
recordActivity,
seedDefaultActivitiesIfEmpty,
} from "@/lib/activity-store";
import type { ActivityType } from "@/types/activity";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function GET(request: Request) {
const { response: rateLimitResponse, headers: rateLimitHeaders } =
checkRateLimit(request);
if (rateLimitResponse) {
return rateLimitResponse;
}

const { searchParams } = new URL(request.url);
const userId = (searchParams.get("userId") ?? "").trim();

if (!userId) {
return buildNoStoreJson(
{
ok: false,
error: "userId query parameter is required.",
},
400,
rateLimitHeaders,
);
}

// Seed default activities for demo user if none exist yet
seedDefaultActivitiesIfEmpty(userId);

const type = (searchParams.get("type") as ActivityType) || undefined;
const limitParam = searchParams.get("limit");
const offsetParam = searchParams.get("offset");

const limit = limitParam ? parseInt(limitParam, 10) : 50;
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;

const result = listUserActivities({
userId,
type,
limit,
offset,
});

return buildNoStoreJson(
{
ok: true,
activities: result.activities,
total: result.total,
},
200,
rateLimitHeaders,
);
}

export async function POST(request: Request) {
const { response: rateLimitResponse, headers: rateLimitHeaders } =
checkRateLimit(request);
if (rateLimitResponse) {
return rateLimitResponse;
}

let body: unknown;
try {
body = await request.json();
} catch {
return buildNoStoreJson(
{
ok: false,
error: "Request body must be valid JSON.",
},
400,
rateLimitHeaders,
);
}

if (!body || typeof body !== "object") {
return buildNoStoreJson(
{
ok: false,
error: "Invalid activity payload.",
},
400,
rateLimitHeaders,
);
}

const payload = body as Record<string, unknown>;
const userId = String(payload.userId ?? "").trim();
const type = String(payload.type ?? "") as ActivityType;
const title = String(payload.title ?? "").trim();
const description = String(payload.description ?? "").trim();
const metadata = (payload.metadata ?? undefined) as Record<string, unknown> | undefined;

if (!userId) {
return buildNoStoreJson(
{ ok: false, error: "userId is required." },
400,
rateLimitHeaders,
);
}

if (!type) {
return buildNoStoreJson(
{ ok: false, error: "type is required." },
400,
rateLimitHeaders,
);
}

if (!title) {
return buildNoStoreJson(
{ ok: false, error: "title is required." },
400,
rateLimitHeaders,
);
}

const activity = recordActivity({
userId,
type,
title,
description,
metadata,
});

return buildNoStoreJson(
{
ok: true,
activity,
},
201,
rateLimitHeaders,
);
}
Loading
Loading