Skip to content
Merged

Dev #372

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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ dist/
next-env.d.ts

logs/
*.log
*.log
# Firebase service account
serviceAccountKey.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import redisClient from "../config/redis";
import { ErrorResponse } from "./responses";
import logger from "./logger";
import { ErrorResponse } from "../utils/responses";
import logger from "../utils/logger";

export const rateLimiter = (
prefix: string,
Expand All @@ -10,15 +10,13 @@ export const rateLimiter = (
getIdentifier?: (req: Request) => string | undefined,
) => {
return async (req: Request, res: Response, next: NextFunction) => {
const identifier = getIdentifier?.(req) || req.auth?.id || req.ip;
const identifier = getIdentifier?.(req) || req.auth?.id;

if (!identifier) {
next(); // Should not happen with req.ip fallback
next();
return;
}

// Key format: ratelimit:{prefix}:{identifier}:{window_start_timestamp}
// We use a fixed-window approach where the window is identified by the floor of the current timestamp
const now = Math.floor(Date.now() / 1000);
const windowStart = now - (now % windowSeconds);
const key = `ratelimit:${prefix}:${identifier}:${windowStart}`;
Expand All @@ -27,7 +25,6 @@ export const rateLimiter = (
const current = await redisClient.incr(key);

if (current === 1) {
// First request in this window, set expiration
await redisClient.expire(key, windowSeconds);
}

Expand All @@ -50,7 +47,6 @@ export const rateLimiter = (
next();
} catch (error) {
logger.error(`[RateLimit] Error for ${key}:`, error);
// Fail open: allow the request if Redis is down, but log the error
next();
}
};
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/analytics.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import express from "express";
import { authenticate } from "../middleware/auth.middleware";
import * as analyticsController from "../controllers/analytics.controller";

import { rateLimiter } from "../utils/ratelimit";
import { rateLimiter } from "../middleware/ratelimit.middleware";

const router = express.Router();

Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/auth.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
logoutDevice,
} from "../controllers/auth.controller";
import { authenticate } from "../middleware/auth.middleware";
import { rateLimiter } from "../utils/ratelimit";
import { rateLimiter } from "../middleware/ratelimit.middleware";

import { validate } from "../middleware/validate.middleware";
import {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/generative.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "../controllers/generative.controller";
import { authenticate } from "../middleware/auth.middleware";
import { checkUserGenerationLimit } from "../middleware/limitChecker.middleware";
import { rateLimiter } from "../utils/ratelimit";
import { rateLimiter } from "../middleware/ratelimit.middleware";

const router = Router();

Expand Down
24 changes: 7 additions & 17 deletions backend/src/services/cron/analytics.cron.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// import cron from "node-cron";
import cron from "node-cron";
import logger from "../../utils/logger";
import * as SocialAccountRepository from "../../repositories/socialAccount.repository";
import * as postRepository from "../../repositories/post.repository";
Expand All @@ -9,14 +9,14 @@ export class AnalyticsCronService {
logger.info("📅 Initializing Analytics Cron Service...");

// 1. Post Analytics Job - Run every 2 hours to support smart fetching
// cron.schedule("0 */2 * * *", async () => {
// await this.schedulePostAnalyticsTasks();
// });
cron.schedule("0 */2 * * *", async () => {
await this.schedulePostAnalyticsTasks();
});

// 2. Account Analytics Job - Run once a day at 12:00 AM
// cron.schedule("0 0 * * *", async () => {
// await this.scheduleAccountAnalyticsTasks();
// });
cron.schedule("0 0 * * *", async () => {
await this.scheduleAccountAnalyticsTasks();
});

logger.info("✅ Analytics Cron Jobs ready (Automated scheduling disabled in DEV).");
}
Expand Down Expand Up @@ -58,9 +58,6 @@ export class AnalyticsCronService {
}
}

/**
* Helper to check if a specific platform status within a post needs an update
*/
private static checkSpecificPlatformNeedsUpdate(
postCreatedAt: Date | undefined,
lastFetch?: Date,
Expand All @@ -75,29 +72,22 @@ export class AnalyticsCronService {
const ONE_DAY = 24 * 60 * 60 * 1000;
const SEVEN_DAYS = 7 * ONE_DAY;

// Fresh (< 24h): 2h interval
if (ageMs < ONE_DAY) {
return diffMs >= 2 * 60 * 60 * 1000;
}
// Recent (1-7d): 12h interval
if (ageMs < SEVEN_DAYS) {
return diffMs >= 12 * 60 * 60 * 1000;
}
// Old (> 7d): 24h interval
return diffMs >= ONE_DAY;
}

/**
* Schedules account-level metrics fetch (followers) for all connected accounts
*/
public static async scheduleAccountAnalyticsTasks() {
logger.info("🔄 Checking for accounts needing follower updates...");

try {
const accounts = await SocialAccountRepository.findAll();

for (const account of accounts) {
// Collect platforms where at least one sub-platform is connected
const platforms: string[] = [];

if (account.facebook?.connected) platforms.push("facebook");
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/(user)/calendar/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function CalendarPage() {
{/* Main Content Area */}
<div className="flex-1 flex flex-col h-full bg-[#F7F7F7] rounded-[2.5rem] overflow-hidden">
{user && (
<div className="px-4 pt-6 lg:px-8 lg:pt-8 bg-[#F7F7F7] lg:hidden">
<div className="px-3 pt-4 sm:px-4 sm:pt-6 lg:px-8 lg:pt-8 bg-[#F7F7F7] lg:hidden">
<Header
userName={user.name}
userEmail={user.email}
Expand All @@ -61,7 +61,7 @@ export default function CalendarPage() {
)}

{/* Calendar Content */}
<main className="flex-1 px-4 py-6 lg:px-8 lg:py-8 overflow-y-auto custom-scrollbar">
<main className="flex-1 px-3 py-4 sm:px-4 sm:py-6 lg:px-8 lg:py-8 overflow-y-auto custom-scrollbar">
{!user ? (
<div className="flex items-center justify-center h-full">
<LoadingH />
Expand Down
28 changes: 14 additions & 14 deletions frontend/src/components/calendar/CalendarComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,24 @@ export default function CalendarComponent() {
};

return (
<div className="flex flex-col gap-6 md:gap-8 max-w-7xl mx-auto">
<div className="flex flex-col gap-4 sm:gap-6 md:gap-8 max-w-7xl mx-auto min-w-0 overflow-hidden">
{/* Calendar Header matching mockup */}
<div className="flex items-center justify-between px-2 pt-2">
<div className="flex items-center justify-between px-1 sm:px-2 pt-2 gap-2">
{/* Year Label */}
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 tracking-tight">
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 tracking-tight shrink-0">
{format(currentDate, "yyyy")}
</h2>

{/* Month Navigation Pill */}
<div className="flex items-center bg-white rounded-full px-1 py-1 shadow-sm border border-gray-100">
<div className="flex items-center bg-white rounded-full px-0.5 sm:px-1 py-1 shadow-sm border border-gray-100 min-w-0">
<button
onClick={prevMonth}
className="p-2 hover:bg-gray-50 rounded-full transition-all group"
>
<ChevronLeft size={20} className="text-gray-400 group-hover:text-gray-900" />
</button>

<span className="px-4 text-lg md:text-xl font-bold text-gray-900 lowercase min-w-[120px] text-center">
<span className="px-2 sm:px-4 text-base sm:text-lg md:text-xl font-bold text-gray-900 lowercase min-w-[80px] sm:min-w-[120px] text-center truncate">
{format(currentDate, "MMMM")}
</span>

Expand All @@ -120,7 +120,7 @@ export default function CalendarComponent() {
</div>

{/* Calendar Grid Container */}
<div className="bg-[#F8F9FA] md:bg-[#F9FAFB] rounded-[2.5rem] p-4 md:p-8">
<div className="bg-[#F8F9FA] md:bg-[#F9FAFB] rounded-2xl sm:rounded-[2.5rem] p-2 sm:p-4 md:p-8">
{/* Days Header */}
<div className="grid grid-cols-7 mb-4">
{(isMobile
Expand All @@ -129,15 +129,15 @@ export default function CalendarComponent() {
).map((day) => (
<div
key={day}
className="text-center text-[11px] md:text-[14px] font-semibold text-gray-900 md:text-gray-700 py-2"
className="text-center text-[10px] sm:text-[11px] md:text-[14px] font-semibold text-gray-900 md:text-gray-700 py-1.5 sm:py-2"
>
{day}
</div>
))}
</div>

{/* Days Grid */}
<div className="grid grid-cols-7 gap-1 md:gap-3">
<div className="grid grid-cols-7 gap-0.5 sm:gap-1 md:gap-3">
{calendarDays.map((day, idx) => {
const isSelected = isSameDay(day, selectedDate);
const isCurrentMonth = isSameMonth(day, monthStart);
Expand Down Expand Up @@ -171,9 +171,9 @@ export default function CalendarComponent() {
key={idx}
onClick={() => setSelectedDate(day)}
className={cn(
"relative flex flex-col transition-all duration-300",
"relative flex items-center justify-center md:flex-col md:items-stretch md:justify-start transition-all duration-300",
"aspect-square md:aspect-[1.3/1]",
"rounded-full p-2 md:rounded-[18px] md:p-4",
"rounded-full p-1 sm:p-2 md:rounded-[18px] md:p-4",
!isCurrentMonth ? "opacity-30 md:bg-white/40" : "opacity-100",
isSelected
? "bg-[#318D62] text-white shadow-lg md:shadow-none"
Expand All @@ -185,7 +185,7 @@ export default function CalendarComponent() {
{/* Date Number - Top Left on Desktop, Center on Mobile */}
<span
className={cn(
"text-[14px] md:text-[15px] font-bold z-20 transition-all duration-300",
"text-[12px] sm:text-[14px] md:text-[15px] font-bold z-20 transition-all duration-300",
"md:absolute md:top-3 md:left-4",
isSelected
? "text-white drop-shadow-md"
Expand Down Expand Up @@ -328,8 +328,8 @@ export default function CalendarComponent() {

{/* Posts Section */}
<div className="space-y-4">
<div className="flex items-center justify-between px-2 mb-2">
<h3 className="text-xl font-bold text-gray-900">
<div className="flex items-center justify-between px-1 sm:px-2 mb-2 gap-2">
<h3 className="text-lg sm:text-xl font-bold text-gray-900 truncate min-w-0">
{isSameDay(selectedDate, new Date())
? "Today's Content"
: format(selectedDate, "MMMM do")}
Expand Down Expand Up @@ -380,7 +380,7 @@ export default function CalendarComponent() {
</div>
</div>
) : (
<div className="bg-white rounded-[2.5rem] p-12 flex flex-col items-center justify-center text-center border border-gray-50 shadow-sm">
<div className="bg-white rounded-2xl sm:rounded-[2.5rem] p-6 sm:p-12 flex flex-col items-center justify-center text-center border border-gray-50 shadow-sm">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mb-4">
<FileText size={32} className="text-gray-200" />
</div>
Expand Down
36 changes: 24 additions & 12 deletions frontend/src/components/calendar/CalendarPostCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import Image from "next/image";
import { useRouter } from "next/navigation";
import { ArrowUpRight } from "lucide-react";

interface CalendarPostCardProps {
Expand All @@ -13,18 +14,29 @@ interface CalendarPostCardProps {
}

export function CalendarPostCard({
id,
imageUrl,
description,
platformStatuses = [],
onClick,
}: CalendarPostCardProps) {
const router = useRouter();

const handleClick = () => {
if (onClick) {
onClick();
} else {
router.push(`/history/${id}`);
}
};

return (
<div
onClick={onClick}
className="bg-white rounded-[32px] p-4 flex gap-4 cursor-pointer hover:shadow-md transition-all border border-gray-50 mb-4"
onClick={handleClick}
className="bg-white rounded-2xl sm:rounded-[32px] p-3 sm:p-4 flex gap-3 sm:gap-4 cursor-pointer hover:shadow-md transition-all border border-gray-50 mb-4"
>
{/* Post Image */}
<div className="relative w-36 h-36 rounded-[24px] overflow-hidden flex-shrink-0 bg-gray-100">
<div className="relative w-28 h-28 sm:w-36 sm:h-36 rounded-2xl sm:rounded-[24px] overflow-hidden flex-shrink-0 bg-gray-100">
{imageUrl ? (
<Image src={imageUrl} alt="Post Content" fill className="object-cover" />
) : (
Expand All @@ -49,23 +61,23 @@ export function CalendarPostCard({
</div>

{/* Post Details */}
<div className="flex-1 flex flex-col justify-between py-1">
<div className="flex-1 flex flex-col justify-between py-1 min-w-0">
<div className="space-y-2">
<p className="text-[#1A1A1A] text-[15px] leading-[1.4] font-medium line-clamp-3">
<p className="text-[#1A1A1A] text-[13px] sm:text-[15px] leading-[1.4] font-medium line-clamp-3">
{description}
</p>
</div>

<div className="flex items-end justify-between mt-auto">
<div className="space-y-2">
<span className="text-[12px] font-semibold text-gray-400 uppercase tracking-wider">
<div className="flex items-end justify-between mt-auto gap-2">
<div className="space-y-1.5 min-w-0">
<span className="text-[11px] sm:text-[12px] font-semibold text-gray-400 uppercase tracking-wider">
posted
</span>
<div className="flex -space-x-2">
<div className="flex -space-x-1.5 sm:-space-x-2 flex-wrap">
{platformStatuses.map((p, idx) => (
<div
key={idx}
className="relative w-8 h-8 rounded-full border-2 border-white overflow-hidden bg-white shadow-sm"
className="relative w-6 h-6 sm:w-8 sm:h-8 rounded-full border-2 border-white overflow-hidden bg-white shadow-sm"
title={p.platform}
>
<Image
Expand All @@ -79,8 +91,8 @@ export function CalendarPostCard({
</div>
</div>

<button className="w-9 h-9 rounded-full border border-gray-100 flex items-center justify-center text-gray-900 bg-white hover:bg-gray-50 transition-colors shadow-sm">
<ArrowUpRight size={18} />
<button className="w-8 h-8 sm:w-9 sm:h-9 rounded-full border border-gray-100 flex items-center justify-center text-gray-900 bg-white hover:bg-gray-50 transition-colors shadow-sm shrink-0">
<ArrowUpRight size={16} className="sm:w-[18px] sm:h-[18px]" />
</button>
</div>
</div>
Expand Down