From 3284c9aef1b0139f289b25c47a83d2c89e707058 Mon Sep 17 00:00:00 2001 From: Priyanshu Verma Date: Wed, 24 Sep 2025 12:08:23 +0000 Subject: [PATCH 1/3] index on master: fb011e1 fix: removed heroui react dep --- src/app/posts/page.tsx | 301 +++++++++++++++++++ src/components/posts/PostActivityTracker.tsx | 199 ++++++++++++ src/components/posts/PostCard.tsx | 90 ++++++ src/hooks/data/use-posts.ts | 78 +++++ 4 files changed, 668 insertions(+) create mode 100644 src/app/posts/page.tsx create mode 100644 src/components/posts/PostActivityTracker.tsx create mode 100644 src/components/posts/PostCard.tsx create mode 100644 src/hooks/data/use-posts.ts diff --git a/src/app/posts/page.tsx b/src/app/posts/page.tsx new file mode 100644 index 0000000..ae35b41 --- /dev/null +++ b/src/app/posts/page.tsx @@ -0,0 +1,301 @@ +"use client"; +import { usePosts, Post, Platform, PostStatus } from "@/hooks/data/use-posts"; +import React, { useState, useMemo } from "react"; +import { Card, CardBody, CardHeader } from "@heroui/card"; +import { Button } from "@heroui/button"; +import { Input } from "@heroui/input"; +import { Skeleton } from "@heroui/skeleton"; +import { Pagination } from "@heroui/pagination"; +import { Select, SelectItem } from "@heroui/select"; +import { Chip } from "@heroui/chip"; +import { Search, AlertCircle, FileText } from "lucide-react"; +import PostCard from "@/components/posts/PostCard"; +import PostActivityTracker from "@/components/posts/PostActivityTracker"; + +function PostGridSkeleton() { + return ( +
+ {Array.from({ length: 8 }).map((_, index) => ( + + + +
+ + +
+
+ +
+ +
+ + +
+
+ + + +
+
+
+
+ ))} +
+ ); +} + +export default function PostsPage() { + const [currentPage, setCurrentPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedPlatform, setSelectedPlatform] = useState( + "ALL" + ); + const [selectedStatus, setSelectedStatus] = useState( + "ALL" + ); + + const pageSize = 12; + + const { data, isLoading, error } = usePosts({ + page: currentPage, + pageSize, + }); + + // Filter posts based on search and filters + const filteredPosts = useMemo(() => { + if (!data?.posts) return []; + + return data.posts.filter((post) => { + const matchesSearch = + post.text.toLowerCase().includes(searchQuery.toLowerCase()) || + post.platform.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesPlatform = + selectedPlatform === "ALL" || post.platform === selectedPlatform; + const matchesStatus = + selectedStatus === "ALL" || post.status === selectedStatus; + + return matchesSearch && matchesPlatform && matchesStatus; + }); + }, [data?.posts, searchQuery, selectedPlatform, selectedStatus]); + + const handlePostView = (post: Post) => { + console.log("View post:", post); + }; + + if (error) { + return ( +
+ + + +

+ Unable to Load Posts +

+

+ There was an error loading your posts. Please check your + connection and try again. +

+ +
+
+
+ ); + } + + return ( +
+
+
+

+ Recent Posts +

+

+ Your social media posts in one place +

+
+ +
+ +
+ +
+
+ } + value={searchQuery} + onValueChange={setSearchQuery} + variant="flat" + size="sm" + className="flex-1" + /> + +
+ + + +
+
+ + {/* Active Filters */} + {(searchQuery || + selectedPlatform !== "ALL" || + selectedStatus !== "ALL") && ( +
+ {searchQuery && ( + setSearchQuery("")} + className="text-xs" + > + "{searchQuery}" + + )} + {selectedPlatform !== "ALL" && ( + setSelectedPlatform("ALL")} + className="text-xs" + > + {selectedPlatform} + + )} + {selectedStatus !== "ALL" && ( + setSelectedStatus("ALL")} + className="text-xs" + > + {selectedStatus} + + )} +
+ )} +
+ +
+ {isLoading ? ( + + ) : filteredPosts.length > 0 ? ( +
+ {filteredPosts.map((post) => ( + + ))} +
+ ) : ( + + +
+
+ +
+
+

+ {searchQuery || + selectedPlatform !== "ALL" || + selectedStatus !== "ALL" + ? "No posts match your filters" + : "No posts created yet"} +

+

+ {searchQuery || + selectedPlatform !== "ALL" || + selectedStatus !== "ALL" + ? "Try adjusting your search criteria or clear the filters to see more posts." + : "Get started by creating your first social media post. Share your content across multiple platforms effortlessly."} +

+
+
+ {(searchQuery || + selectedPlatform !== "ALL" || + selectedStatus !== "ALL") && ( + + )} +
+
+
+
+ )} +
+ + {data && data.totalPages > 1 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/posts/PostActivityTracker.tsx b/src/components/posts/PostActivityTracker.tsx new file mode 100644 index 0000000..2f3b65b --- /dev/null +++ b/src/components/posts/PostActivityTracker.tsx @@ -0,0 +1,199 @@ +"use client"; +import React, { useMemo } from "react"; +import { Tooltip } from "@heroui/tooltip"; +import { Post } from "@/hooks/data/use-posts"; + +interface ActivityDay { + date: Date; + count: number; + posts: Post[]; +} + +// Helper function to calculate current streak +function getCurrentStreak(calendar: ActivityDay[]): number { + let streak = 0; + const today = new Date().toDateString(); + + // Start from today and go backwards + for (let i = calendar.length - 1; i >= 0; i--) { + const day = calendar[i]; + if (day.count > 0) { + streak++; + } else if (day.date.toDateString() !== today || streak > 0) { + // Break streak if no posts (unless it's today and streak is 0) + break; + } + } + + return streak; +} + +interface PostActivityTrackerProps { + posts?: Post[]; + activityData?: ActivityDay[]; // Optional server-provided data + className?: string; +} + +export default function PostActivityTracker({ + posts = [], + activityData, +}: PostActivityTrackerProps) { + // Generate the last 365 days of activity data + const activityCalendar = useMemo(() => { + const today = new Date(); + const oneYearAgo = new Date(); + oneYearAgo.setFullYear(today.getFullYear() - 1); + + // Use server data if provided, otherwise aggregate from posts + if (activityData) { + return activityData; + } + + // Client-side aggregation + const calendar: ActivityDay[] = []; + const postsByDate = new Map(); + + // Group posts by date + posts.forEach((post) => { + const dateKey = new Date(post.createdAt).toDateString(); + if (!postsByDate.has(dateKey)) { + postsByDate.set(dateKey, []); + } + postsByDate.get(dateKey)!.push(post); + }); + + // Generate calendar for the last 365 days + for (let i = 365; i >= 0; i--) { + const date = new Date(); + date.setDate(today.getDate() - i); + const dateKey = date.toDateString(); + const dayPosts = postsByDate.get(dateKey) || []; + + calendar.push({ + date: new Date(date), + count: dayPosts.length, + posts: dayPosts, + }); + } + + return calendar; + }, [posts, activityData]); + + // Get intensity class based on post count + const getIntensityClass = (count: number) => { + if (count === 0) return "bg-default-100 border border-default-200"; + if (count <= 2) return "bg-primary-200 border border-primary-300"; + if (count <= 4) return "bg-primary-400 border border-primary-500"; + if (count <= 6) return "bg-primary-600 border border-primary-700"; + return "bg-primary-800 border border-primary-900"; + }; + + // Group days by weeks for proper grid layout + const weeks = useMemo(() => { + const weeks: ActivityDay[][] = []; + let currentWeek: ActivityDay[] = []; + + activityCalendar.forEach((day, index) => { + const dayOfWeek = day.date.getDay(); // 0 = Sunday, 1 = Monday, etc. + + if (index === 0) { + // Fill in empty days at the beginning of the first week + for (let i = 0; i < dayOfWeek; i++) { + currentWeek.push({ + date: new Date(0), + count: 0, + posts: [], + }); + } + } + + currentWeek.push(day); + + if (dayOfWeek === 6 || index === activityCalendar.length - 1) { + // End of week (Saturday) or last day + weeks.push([...currentWeek]); + currentWeek = []; + } + }); + + return weeks; + }, [activityCalendar]); + + const totalPosts = activityCalendar.reduce((sum, day) => sum + day.count, 0); + + return ( +
+
+
+

Activity

+

+ {totalPosts} posts in the last year +

+
+
+ +
+
+ {weeks.map((week, weekIndex) => ( +
+ {week.map((day, dayIndex) => { + if (day.date.getTime() === 0) { + return ( +
+ ); + } + + return ( + +
+ {day.date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + })} +
+
+ {day.count} post{day.count !== 1 ? "s" : ""} +
+
+ } + delay={200} + closeDelay={100} + > +
+ + ); + })} +
+ ))} +
+ +
+
+ {getCurrentStreak(activityCalendar)} day streak +
+
+ Less +
+
+
+
+
+
+
+ More +
+
+
+
+ ); +} diff --git a/src/components/posts/PostCard.tsx b/src/components/posts/PostCard.tsx new file mode 100644 index 0000000..980e871 --- /dev/null +++ b/src/components/posts/PostCard.tsx @@ -0,0 +1,90 @@ +"use client"; +import React from "react"; +import { Card, CardBody, CardHeader, CardFooter } from "@heroui/card"; +import { Avatar } from "@heroui/avatar"; +import { Chip } from "@heroui/chip"; +import { Post, PostStatus } from "@/hooks/data/use-posts"; + +import moment from "moment"; +import { authClient } from "@/lib/auth-client"; +import { cn } from "@/lib/utils"; +import { getPlatform } from "@/lib/icons"; + +interface PostCardProps { + post: Post; + onView?: (post: Post) => void; +} + +export default function PostCard({ post, onView }: PostCardProps) { + const { data: session } = authClient.useSession(); + const platformInfo = getPlatform(post.platform); + return ( + { + onView && onView(post); + }} + > + +
+ +
+

{session?.user.name || "User"}

+

+ {moment(post.createdAt).fromNow()} +

+
+
+ +
+ +
+

+ {post.text.slice(0, 200)} + ... +

+
+ +
+
+ + {post.status} + +
+
+ {post.imageUrl && ( + Post Image + )} +
+
+
+ +
+
+
+ ); +} diff --git a/src/hooks/data/use-posts.ts b/src/hooks/data/use-posts.ts new file mode 100644 index 0000000..2b19381 --- /dev/null +++ b/src/hooks/data/use-posts.ts @@ -0,0 +1,78 @@ +import fetcher from "@/lib/fetcher"; +import { useQuery } from "@tanstack/react-query"; + +export interface PostPaginationResponse { + posts: Post[]; + totalCount: number; + page: number; + pageSize: number; + totalPages: number; +} + +export function usePosts(filters?: { page?: number; pageSize?: number }) { + return useQuery({ + queryKey: [ + "posts", + { page: filters?.page || 1, pageSize: filters?.pageSize || 10 }, + ], + queryFn: async () => + fetcher("GET", "/posts", { + searchParams: { + page: filters?.page || 1, + pageSize: filters?.pageSize || 10, + }, + }), + }); +} + +export const Platform = { + LINKEDIN: "LINKEDIN", + TWITTER: "TWITTER", + INSTAGRAM: "INSTAGRAM", + DEVTO: "DEVTO", + MEDIUM: "MEDIUM", + FACEBOOK: "FACEBOOK", + X: "X", +} as const; + +export type Platform = (typeof Platform)[keyof typeof Platform]; + +export const PostStatus = { + CREATED: "CREATED", + DRAFTING: "DRAFTING", + DRAFT: "DRAFT", + READY: "READY", + SCHEDULED: "SCHEDULED", + PUBLISHING: "PUBLISHING", + PUBLISHED: "PUBLISHED", + FAILED: "FAILED", + CANCELED: "CANCELED", +} as const; + +export type PostStatus = (typeof PostStatus)[keyof typeof PostStatus]; + +export const ContentType = { + TEXT: "TEXT", + IMAGE: "IMAGE", + VIDEO: "VIDEO", + DOCUMENT: "DOCUMENT", + POLL: "POLL", +} as const; + +export type ContentType = (typeof ContentType)[keyof typeof ContentType]; + +export type Post = { + id: string; + text: string; + contentType: ContentType; + imageUrl: string | null; + videoUrl: string | null; + platform: Platform; + externalPostId: string | null; + status: PostStatus; + createdAt: Date; + updatedAt: Date; + generatedAt: Date | null; + isAIgenerated: boolean; + scheduleId: string | null; +}; From 5b5a5fb1279717ea2025292e82b1b3d184020b9e Mon Sep 17 00:00:00 2001 From: Priyanshu Verma Date: Wed, 24 Sep 2025 12:53:07 +0000 Subject: [PATCH 2/3] feat: posts page --- src/components/sidebar/index.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/sidebar/index.tsx b/src/components/sidebar/index.tsx index 4370c91..bfa4f13 100644 --- a/src/components/sidebar/index.tsx +++ b/src/components/sidebar/index.tsx @@ -2,17 +2,14 @@ import React from "react"; import { usePathname } from "next/navigation"; import { useSidebar } from "./sidebar-provider"; import { - ChevronLeft, - ChevronRight, GalleryVerticalEndIcon, Home, - Inbox, - HelpCircle, Settings, TimerIcon, SidebarOpenIcon, SidebarCloseIcon, BellDotIcon, + LayoutGrid, } from "lucide-react"; import { Drawer, DrawerContent } from "@heroui/drawer"; import { Button } from "@heroui/button"; @@ -23,6 +20,7 @@ import Brand from "../shared/brand"; import { ThemeSwitch } from "../shared/theme-switch"; import ProfileCard from "../shared/profile-card"; import { useDisclosure } from "@heroui/modal"; +import { FaBlog } from "react-icons/fa"; interface NavItem { icon: React.ElementType; @@ -44,12 +42,8 @@ const navigationData: NavSection[] = [ items: [ { icon: Home, label: "Dashboard", href: "/" }, { icon: TimerIcon, label: "Schedules", href: "/schedules" }, - - { - icon: GalleryVerticalEndIcon, - label: "Gallery", - href: "/gallery", - }, + { icon: GalleryVerticalEndIcon, label: "Gallery", href: "/gallery" }, + { icon: LayoutGrid, label: "Posts", href: "/posts" }, ], }, { From 8f2627dac3f706076591b22c830f217442e6f6ec Mon Sep 17 00:00:00 2001 From: Priyanshu Verma Date: Wed, 24 Sep 2025 13:53:47 +0000 Subject: [PATCH 3/3] feat: vercel workflow and user preset --- .github/workflows/vercel-pull-request.yml | 2 +- src/app/personalize/page.tsx | 266 +++++++++++++++++----- src/components/sidebar/index.tsx | 79 ++++++- src/hooks/data/use-presets.ts | 25 +- 4 files changed, 297 insertions(+), 75 deletions(-) diff --git a/.github/workflows/vercel-pull-request.yml b/.github/workflows/vercel-pull-request.yml index 02eafaa..54fb246 100644 --- a/.github/workflows/vercel-pull-request.yml +++ b/.github/workflows/vercel-pull-request.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: amondnet/vercel-action@v20 + - uses: amondnet/vercel-action@v25 id: vercel-deploy with: vercel-token: ${{ secrets.VERCEL_TOKEN }} diff --git a/src/app/personalize/page.tsx b/src/app/personalize/page.tsx index d6fa9a1..e976274 100644 --- a/src/app/personalize/page.tsx +++ b/src/app/personalize/page.tsx @@ -5,15 +5,41 @@ import { Button } from "@heroui/button"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Input } from "@heroui/input"; import { Textarea } from "@heroui/input"; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal"; -import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from "@heroui/dropdown"; +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, +} from "@heroui/modal"; +import { + Dropdown, + DropdownTrigger, + DropdownMenu, + DropdownItem, +} from "@heroui/dropdown"; import { Avatar } from "@heroui/avatar"; import { Badge } from "@heroui/badge"; import { Chip } from "@heroui/chip"; import { Divider } from "@heroui/divider"; -import { Plus, Search, Edit, Trash2, Copy, MoreVertical, User, Sparkles, Calendar, Settings } from "lucide-react"; +import { + Plus, + Search, + Edit, + Trash2, + Copy, + MoreVertical, + User, + Sparkles, + Calendar, +} from "lucide-react"; import { useUser } from "@/hooks/data/use-user"; -import { useGetPresets, useCreatePreset, useUpdatePreset, useDeletePreset, useUpdateUserProfile } from "@/hooks/data/use-presets"; +import { + useCreatePreset, + useUpdatePreset, + useDeletePreset, + useUpdateUserProfile, +} from "@/hooks/data/use-presets"; import LoaderSection from "@/components/shared/loader-section"; import { addToast } from "@heroui/toast"; @@ -24,14 +50,17 @@ export default function PresetsPage() { const [selectedPreset, setSelectedPreset] = useState(null); const [isEditingProfile, setIsEditingProfile] = useState(false); - const { data: userProfile, isLoading: userLoading, isError, error } = useUser(); + const { + data: userProfile, + isLoading: userLoading, + isError, + error, + } = useUser(); const createPresetMutation = useCreatePreset(); const updatePresetMutation = useUpdatePreset(); const deletePresetMutation = useDeletePreset(); const updateProfileMutation = useUpdateUserProfile(); - console.log("user", userProfile); - const [presetForm, setPresetForm] = useState({ title: "", description: "", @@ -59,11 +88,6 @@ export default function PresetsPage() { description: presetForm.description, }); - addToast({ - title: "Preset created successfully!", - color: "success", - }); - setPresetForm({ title: "", description: "" }); setIsCreateModalOpen(false); } catch (error) { @@ -85,7 +109,11 @@ export default function PresetsPage() { }; const handleUpdatePreset = async () => { - if (selectedPreset && presetForm.title.trim() && presetForm.description.trim()) { + if ( + selectedPreset && + presetForm.title.trim() && + presetForm.description.trim() + ) { try { await updatePresetMutation.mutateAsync({ title: presetForm.title, @@ -155,22 +183,6 @@ export default function PresetsPage() { } }; - interface FormatDateOptions { - month: "short" | "numeric" | "2-digit" | "long" | "narrow"; - day: "numeric" | "2-digit"; - year: "numeric" | "2-digit"; - } - - const formatDate = (date: Date): string | undefined => { - const options: FormatDateOptions = { - month: "short", - day: "numeric", - year: "numeric", - }; - if (!date) return; - return new Intl.DateTimeFormat("en-US", options).format(date); - }; - const isLoading = userLoading; if (isLoading) return ; @@ -178,18 +190,27 @@ export default function PresetsPage() { if (!userProfile) { return (
-

Failed to load user profile. Please try again.

+

+ Failed to load user profile. Please try again. +

); } return ( -
+

Presets

-

Manage your AI personalities for customized content generation

+

+ Manage your AI personalities for customized content generation +

-
@@ -206,7 +227,10 @@ export default function PresetsPage() { variant="light" size="sm" onPress={() => { - setProfileForm({ name: userProfile.name, bio: userProfile.bio || "" }); + setProfileForm({ + name: userProfile.name, + bio: userProfile.bio || "", + }); setIsEditingProfile(!isEditingProfile); }} > @@ -216,26 +240,65 @@ export default function PresetsPage() {
- +
{isEditingProfile ? (
- setProfileForm({ ...profileForm, name: e.target.value })} variant="bordered" isDisabled={updateProfileMutation.isPending} /> -