diff --git a/instrumentation-client.ts b/instrumentation-client.ts index b9d0899..b320c1b 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -1,9 +1,9 @@ -import posthog from "posthog-js"; +// import posthog from "posthog-js"; -posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { - api_host: "/ingest", - ui_host: "https://us.posthog.com", - defaults: "2025-05-24", - capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this - debug: process.env.NODE_ENV === "development", -}); +// posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { +// api_host: "/ingest", +// ui_host: "https://us.posthog.com", +// defaults: "2025-05-24", +// capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this +// debug: process.env.NODE_ENV === "development", +// }); diff --git a/next.config.ts b/next.config.ts index fbe2a37..077d7c2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,33 +1,43 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ - eslint: { - ignoreDuringBuilds: true, - }, + /* config options here */ + eslint: { + ignoreDuringBuilds: true, + }, - images: { - remotePatterns: [ - { - hostname: "api.post0.live", - protocol: "https", - pathname: "/**", - }, - ], - }, - async rewrites() { - return [ - { - source: "/ingest/static/:path*", - destination: "https://us-assets.i.posthog.com/static/:path*", - }, - { - source: "/ingest/:path*", - destination: "https://us.i.posthog.com/:path*", - }, - ]; - }, - skipTrailingSlashRedirect: true, + images: { + remotePatterns: [ + { + hostname: "api.post0.live", + protocol: "https", + pathname: "/**", + }, + { + hostname: "images.unsplash.com", + protocol: "https", + pathname: "/**", + }, + { + hostname: "res.cloudinary.com", + protocol: "https", + pathname: "/**", + }, + ], + }, + async rewrites() { + return [ + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + ]; + }, + skipTrailingSlashRedirect: true, }; export default nextConfig; diff --git a/src/app/gallery/page.tsx b/src/app/gallery/page.tsx index 78ffe42..d5b0696 100644 --- a/src/app/gallery/page.tsx +++ b/src/app/gallery/page.tsx @@ -11,7 +11,6 @@ import { Search, Filter } from "lucide-react"; import { AssetType } from "@/types/asset"; import FileUploadZone from "@/components/gallery/FileUploadZone"; import AssetGrid from "@/components/gallery/AssetGrid"; -import GallerySkeleton from "@/components/gallery/GallerySkeleton"; import AssetEditModal from "@/components/gallery/AssetEditModal"; import { Asset, useGalleryAssets } from "@/hooks/data/use-assets"; @@ -83,8 +82,6 @@ export default function GalleryPage() { /> - - {galleryLoading && }
( - "ALL" - ); - const [selectedStatus, setSelectedStatus] = useState( - "ALL" - ); + const [currentPage, setCurrentPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedPlatform, setSelectedPlatform] = useState( + "ALL", + ); + const [selectedStatus, setSelectedStatus] = useState( + "ALL", + ); - const pageSize = 12; + const pageSize = 12; - const { data, isLoading, error } = usePosts({ - page: currentPage, - pageSize, - }); + const { data, isLoading, error } = usePosts({ + page: currentPage, + pageSize, + }); - // Filter posts based on search and filters - const filteredPosts = useMemo(() => { - if (!data?.posts) return []; + // 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 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]); + return matchesSearch && matchesPlatform && matchesStatus; + }); + }, [data?.posts, searchQuery, selectedPlatform, selectedStatus]); - if (error) { - return ( -
- - - -

- Unable to Load Posts -

-

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

-
- - - -
-
- ); - } + 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 -

-
+ return ( +
+
+
+
+

+ Recent Posts +

+

+ Your social media posts in one place +

+
- -
+ +
-
- -
+
+ +
-
-
- } - value={searchQuery} - onValueChange={setSearchQuery} - variant="flat" - size="sm" - className="flex-1" - /> +
+
+ } + 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."} -

-
-
+ {/* Active Filters */} {(searchQuery || - selectedPlatform !== "ALL" || - selectedStatus !== "ALL") && ( - + 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} + + )} +
)} -
-
-
- )} -
- {data && data.totalPages > 1 && ( -
- -
- )} -
-
- ); +
+ {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 && ( +
+ +
+ )} +
+
+ ); } function PostGridSkeleton() { - return ( -
- {Array.from({ length: 8 }).map((_, index) => ( - - - -
- - -
-
- -
- -
- - -
-
- - - -
-
-
-
- ))} -
- ); + return ( +
+ {Array.from({ length: 8 }).map((_, index) => ( + + + +
+ + +
+
+ +
+ +
+ + +
+
+ + + +
+
+
+
+ ))} +
+ ); } diff --git a/src/app/schedules/[scheduleId]/page.tsx b/src/app/schedules/[scheduleId]/page.tsx index bb91b15..f0a3e4a 100644 --- a/src/app/schedules/[scheduleId]/page.tsx +++ b/src/app/schedules/[scheduleId]/page.tsx @@ -8,232 +8,245 @@ import { Card, CardBody, CardHeader } from "@heroui/card"; import { Button } from "@heroui/button"; import { - Dropdown, - DropdownTrigger, - DropdownMenu, - DropdownItem, + Dropdown, + DropdownTrigger, + DropdownMenu, + DropdownItem, } from "@heroui/dropdown"; import { Chip } from "@heroui/chip"; import { Divider } from "@heroui/divider"; import { - Calendar, - Edit, - Trash2, - MoreVertical, - AlertCircle, + Calendar, + Edit, + Trash2, + MoreVertical, + AlertCircle, } from "lucide-react"; import DeleteScheduleModal from "@/components/schedules/modal/DeleteScheduleModal"; import EditScheduleModal from "@/components/schedules/modal/EditScheduleModal"; +import LoaderSection from "@/components/shared/loader-section"; function ScheduleRange({ start, end }: { start: Date; end: Date }) { - const startDate = new Date(start); - const endDate = new Date(end); - - const formatter = new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); - - const shortFormatter = new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - }); - - const sameMonth = startDate.getMonth() === endDate.getMonth(); - const sameYear = startDate.getFullYear() === endDate.getFullYear(); + const startDate = new Date(start); + const endDate = new Date(end); + + const formatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + + const shortFormatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + }); + + const sameMonth = startDate.getMonth() === endDate.getMonth(); + const sameYear = startDate.getFullYear() === endDate.getFullYear(); + + if (sameMonth && sameYear) { + return ( + + {shortFormatter.format(startDate)}–{formatter.format(endDate)} + + ); + } - if (sameMonth && sameYear) { - return ( - - {shortFormatter.format(startDate)}–{formatter.format(endDate)} - - ); - } + if (sameYear) { + return ( + + {shortFormatter.format(startDate)} – {formatter.format(endDate)} + + ); + } - if (sameYear) { return ( - - {shortFormatter.format(startDate)} – {formatter.format(endDate)} - + + {formatter.format(startDate)} – {formatter.format(endDate)} + ); - } - - return ( - - {formatter.format(startDate)} – {formatter.format(endDate)} - - ); } export default function ScheduleDetails() { - const { scheduleId } = useParams(); - const router = useRouter(); - const { - data: schedule, - isLoading, - isError, - } = useSchedule(scheduleId?.toString() || ""); - - // States for modals and editing - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - - // Function to get status badge color - const getStatusColor = (status: string) => { - switch (status) { - case "PUBLISHED": - return "success"; - case "SCHEDULED": - return "primary"; - case "PENDING": - return "warning"; - case "FAILED": - return "danger"; - case "CANCELLED": - return "default"; - default: - return "default"; + const { scheduleId } = useParams(); + const router = useRouter(); + const { + data: schedule, + isLoading, + isError, + } = useSchedule(scheduleId?.toString() || ""); + + // States for modals and editing + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + + // Function to get status badge color + const getStatusColor = (status: string) => { + switch (status) { + case "PUBLISHED": + return "success"; + case "SCHEDULED": + return "primary"; + case "PENDING": + return "warning"; + case "FAILED": + return "danger"; + case "CANCELLED": + return "default"; + default: + return "default"; + } + }; + + if (isLoading) { + return ( +
+ +
+ ); } - }; - - if (isLoading) { - return ( -
-
-
- ); - } - if (isError || !schedule) { - return ( -
- - -
- -

Error loading schedule

-

- We couldn't load the schedule details. Please try again. -

- + if (isError || !schedule) { + return ( +
+ + +
+ +

+ Error loading schedule +

+

+ We couldn't load the schedule details. + Please try again. +

+ +
+
+
- - -
- ); - } - - return ( -
-
- - Schedules - {schedule.title} - - -
- - - - - - - - } - onClick={() => setIsDeleteModalOpen(true)} - > - Delete Schedule - - - -
-
+ ); + } - - -
+ return ( +
-
-

- {schedule.title} -

-
- -
- - {schedule.status} - -
-
- -
- -
-

Date Range

-
- - -
-
-
- - -
-

Settings

-
- - {schedule.timezone} - - - {schedule.imageOption} - -
+ + Schedules + {schedule.title} + + +
+ + + + + + + + } + onClick={() => setIsDeleteModalOpen(true)} + > + Delete Schedule + + +
-
-
- - - - - - - - - setIsDeleteModalOpen(false)} - scheduleId={schedule.id} - /> - - setIsEditModalOpen(false)} - schedule={schedule} - /> -
- ); + + + +
+
+
+

+ {schedule.title} +

+
+ +
+ + {schedule.status} + +
+
+ +
+ +
+

+ Date Range +

+
+ + +
+
+
+ + +
+

+ Settings +

+
+ + {schedule.timezone} + + + {schedule.imageOption} + +
+
+
+
+
+
+ + + + +
+ + setIsDeleteModalOpen(false)} + scheduleId={schedule.id} + /> + + setIsEditModalOpen(false)} + schedule={schedule} + /> +
+ ); } diff --git a/src/app/schedules/page.tsx b/src/app/schedules/page.tsx index dc7a836..5cd55c8 100644 --- a/src/app/schedules/page.tsx +++ b/src/app/schedules/page.tsx @@ -89,22 +89,6 @@ export default function Schedules() {
- -
); } @@ -199,7 +183,10 @@ function ScheduleList({
- + {schedule.status}
diff --git a/src/components/gallery/AssetGrid.tsx b/src/components/gallery/AssetGrid.tsx index 589d13a..4b26985 100644 --- a/src/components/gallery/AssetGrid.tsx +++ b/src/components/gallery/AssetGrid.tsx @@ -1,12 +1,11 @@ "use client"; import React from "react"; -import { Card, CardBody } from "@heroui/card"; -import { Skeleton } from "@heroui/skeleton"; import { Search, FolderOpen } from "lucide-react"; import AssetCard from "./AssetCard"; import { Asset } from "@/hooks/data/use-assets"; +import LoaderSection from "../shared/loader-section"; interface AssetGridProps { assets: Asset[]; @@ -42,33 +41,6 @@ const AssetGrid: React.FC = ({ return `grid gap-2 grid-cols-${sm} sm:grid-cols-${md} lg:grid-cols-${lg} xl:grid-cols-${xl}`; }; - const renderLoadingSkeleton = () => { - return ( -
- {Array.from({ length: 8 }).map((_, index) => ( - - - - -
- - - -
-
- - - -
- -
-
-
- ))} -
- ); - }; - const renderEmptyState = () => { return (
@@ -117,7 +89,9 @@ const AssetGrid: React.FC = ({ if (loading) { return ( -
{renderLoadingSkeleton()}
+
+ +
); } diff --git a/src/components/gallery/FileUploadZone.tsx b/src/components/gallery/FileUploadZone.tsx index 0f09fb6..e6ad857 100644 --- a/src/components/gallery/FileUploadZone.tsx +++ b/src/components/gallery/FileUploadZone.tsx @@ -17,6 +17,7 @@ import { CheckCircle, AlertCircle, RotateCcw, + RefreshCw, } from "lucide-react"; import fetcher from "@/lib/fetcher"; import { getQueryClient } from "@/lib/get-query-client"; @@ -147,9 +148,6 @@ const FileUploadZone: React.FC = ({ onUpload([fileToRetry.file]); }; - const removeFile = (fileId: string) => { - setUploadingFiles((prev) => prev.filter((f) => f.id !== fileId)); - }; const sizeConfig = { compact: { cardPadding: "p-3", @@ -322,23 +320,24 @@ const FileUploadZone: React.FC = ({
)} -
- {isUploading ? ( - - ) : ( - - )} -
-
+
+ {isUploading ? ( + + ) : ( + + )} +

= ({ {isDragActive ? "Drop files here" : isUploading - ? "Uploading..." + ? `Uploading (${ + uploadingFiles.filter((f) => f.status === "uploading") + .length + })` : variant === "compact" ? "Upload files" : "Upload your assets"}

- {variant !== "compact" && ( - <> -

- {isUploading - ? "Please wait while files are being uploaded" - : "Drag and drop files here, or click to browse"} -

-

- Supports images and videos up to {maxFileSize}MB -

- - )} - {variant === "compact" && !isUploading && ( -

- {isDragActive ? "Drop to upload" : `Max ${maxFileSize}MB`} -

- )}
- - {variant === "compact" && isUploading && ( -
- - - { - uploadingFiles.filter((f) => f.status === "uploading") - .length - }{" "} - uploading - -
+ {variant !== "compact" && ( + <> +

+ {isUploading + ? "Please wait while files are being uploaded" + : "Drag and drop files here, or click to browse"} +

+

+ Supports images and videos up to {maxFileSize}MB +

+ + )} + {variant === "compact" && !isUploading && ( +

+ {isDragActive ? "Drop to upload" : `Max ${maxFileSize}MB`} +

)} {variant !== "compact" && ( @@ -402,7 +391,6 @@ const FileUploadZone: React.FC = ({ - {/* Uploading Files Progress */} {uploadingFiles.length > 0 && variant !== "compact" && (

@@ -465,16 +453,6 @@ const FileUploadZone: React.FC = ({

)}

- -
@@ -482,7 +460,6 @@ const FileUploadZone: React.FC = ({
)} - {/* Compact Uploading Files Progress */} {uploadingFiles.length > 0 && variant === "compact" && (
{uploadingFiles.map((uploadingFile) => ( @@ -504,7 +481,6 @@ const FileUploadZone: React.FC = ({ {Math.round(uploadingFile.progress)}% )} - {getStatusIcon(uploadingFile.status)}
@@ -529,22 +505,11 @@ const FileUploadZone: React.FC = ({ className="h-5 px-1 text-xs" onPress={() => retryUpload(uploadingFile)} > - Retry +
)} - - ))} @@ -602,7 +567,6 @@ const FileUploadZone: React.FC = ({ )} - {/* Compact File Rejections */} {fileRejections.length > 0 && variant === "compact" && (
{fileRejections.map(({ file, errors }, index: number) => ( diff --git a/src/components/gallery/GallerySkeleton.tsx b/src/components/gallery/GallerySkeleton.tsx deleted file mode 100644 index e6f3c0b..0000000 --- a/src/components/gallery/GallerySkeleton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import React from "react"; -import { Card, CardBody } from "@heroui/card"; -import { Skeleton } from "@heroui/skeleton"; - -const GallerySkeleton: React.FC = () => { - return ( -
-
- {/* Filters and Search Skeleton */} -
-
- - -
- - -
-
-
- - {/* Assets Grid Skeleton */} -
- -
- -
- {Array.from({ length: 8 }).map((_, index) => ( - - - - -
- - - -
-
- - - -
- -
-
-
- ))} -
-
-
- ); -}; - -export default GallerySkeleton; diff --git a/src/components/gallery/index.ts b/src/components/gallery/index.ts index 6ef6a45..8c65c10 100644 --- a/src/components/gallery/index.ts +++ b/src/components/gallery/index.ts @@ -2,4 +2,3 @@ export { default as FileUploadZone } from "./FileUploadZone"; export { default as AssetCard } from "./AssetCard"; export { default as AssetGrid } from "./AssetGrid"; -export { default as GallerySkeleton } from "./GallerySkeleton"; diff --git a/src/components/posts/modals/PostViewModal.tsx b/src/components/posts/modals/PostViewModal.tsx index 630fa8c..44966b2 100644 --- a/src/components/posts/modals/PostViewModal.tsx +++ b/src/components/posts/modals/PostViewModal.tsx @@ -20,153 +20,158 @@ import { getQueryClient } from "@/lib/get-query-client"; import DeleteModal from "@/components/shared/DeleteModal"; export default function PostViewModal() { - const modal = useViewPostModal(); - const deleteModal = useDeleteModal(); - const { data: session } = authClient.useSession(); - const qc = getQueryClient(); - const post = modal.extras?.post as Post | undefined; - if (!post) return null; + const modal = useViewPostModal(); + const deleteModal = useDeleteModal(); + const { data: session } = authClient.useSession(); + const qc = getQueryClient(); + const post = modal.extras?.post as Post | undefined; + if (!post) return null; - const platformInfo = getPlatform(post.platform); - - async function deletePost() { - try { - if (!post) return; - await fetcher("DELETE", `/posts/${post.id}`); - qc.invalidateQueries({ - queryKey: ["posts"], - }); - modal.onClose(); - } catch (error) { - console.error("Error deleting post:", error); - addToast({ - title: "Problem deleting post", - description: "There was an error deleting the post.", - color: "danger", - }); + const platformInfo = getPlatform(post.platform); + async function deletePost() { + try { + if (!post) return; + await fetcher("DELETE", `/posts/${post.id}`); + qc.invalidateQueries({ + queryKey: ["posts"], + }); + modal.onClose(); + } catch (error) { + console.error("Error deleting post:", error); + addToast({ + title: "Problem deleting post", + description: "There was an error deleting the post.", + color: "danger", + }); + } } - } - - return ( - modal.onClose()} - size="xl" - scrollBehavior="inside" - > - - {(onClose) => ( - <> - -
-

- Post Details -

- - {post.status} - -
-
- - - -
- -
-

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

-

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

-
-
- -
- -
-

- {post.text} -

-
+ return ( + modal.onClose()} + size="xl" + scrollBehavior="inside" + > + + {(onClose) => ( + <> + +
+

+ Post Details +

+ + {post.status} + +
+
+ + + +
+ +
+

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

+

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

+
+
+ +
+ +
+

+ {post.text} +

+
-
-
- {post.imageUrl && ( - Post Image +
+ {post.imageUrl && ( + Post Image + )} +
+
+ + +
+ + +
+
+ + + - )} -
-
- - - {/* Actions like - - delete the post, - - view on platform, - - edit the post (if draft) - - reshare the post - */} -
- - -
-
- - - - - )} - - - ); + + )} + + + ); } diff --git a/src/components/schedules/CalendarView.tsx b/src/components/schedules/CalendarView.tsx index 6c03396..ad6c985 100644 --- a/src/components/schedules/CalendarView.tsx +++ b/src/components/schedules/CalendarView.tsx @@ -7,239 +7,337 @@ import { SchedulePost, useSchedulePosts } from "@/hooks/data/use-schedules"; import PostList from "./PostList"; interface CalendarViewProps { - scheduleId: string; + scheduleId: string; } const CalendarView: React.FC = ({ scheduleId }) => { - const [selectedDate, setSelectedDate] = useState(new Date()); - const [currentDate, setCurrentDate] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [currentDate, setCurrentDate] = useState(new Date()); - const [isPostEditModalOpen, setIsPostEditModalOpen] = useState(false); - const [selectedPost, setSelectedPost] = useState(null); + const [isPostEditModalOpen, setIsPostEditModalOpen] = useState(false); + const [selectedPost, setSelectedPost] = useState(null); - const openPostEditModal = (post: SchedulePost) => { - setSelectedPost(post); - setIsPostEditModalOpen(true); - }; + const openPostEditModal = (post: SchedulePost) => { + setSelectedPost(post); + setIsPostEditModalOpen(true); + }; - const selectedYear = selectedDate.getFullYear(); - const selectedMonth = selectedDate.getMonth(); + const selectedYear = selectedDate.getFullYear(); + const selectedMonth = selectedDate.getMonth(); - const { data: posts, isLoading } = useSchedulePosts(scheduleId, selectedMonth + 1, selectedYear); + const { data: posts, isLoading } = useSchedulePosts( + scheduleId, + selectedMonth + 1, + selectedYear, + ); - const goToPreviousMonth = () => { - setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)); - }; + const goToPreviousMonth = () => { + setCurrentDate( + new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1), + ); + }; - const goToNextMonth = () => { - setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)); - }; + const goToNextMonth = () => { + setCurrentDate( + new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1), + ); + }; - const goToToday = () => { - const today = new Date(); - setCurrentDate(today); - setSelectedDate(today); - }; + const goToToday = () => { + const today = new Date(); + setCurrentDate(today); + setSelectedDate(today); + }; - const handleDateSelect = (date: Date) => { - setSelectedDate(date); - }; + const handleDateSelect = (date: Date) => { + setSelectedDate(date); + }; - const getPostsForSelectedDate = () => { - if (!posts) return []; - return posts.filter((post) => new Date(post.time).toDateString() === selectedDate.toDateString()); - }; + const getPostsForSelectedDate = () => { + if (!posts) return []; + return posts.filter( + (post) => + new Date(post.time).toDateString() === + selectedDate.toDateString(), + ); + }; - const postsByDate: Record = {}; + const postsByDate: Record = {}; - if (posts) { - posts.forEach((post) => { - const postDate = new Date(post.time).toDateString(); - if (!postsByDate[postDate]) { - postsByDate[postDate] = []; - } - postsByDate[postDate].push(post); - }); - } + if (posts) { + posts.forEach((post) => { + const postDate = new Date(post.time).toDateString(); + if (!postsByDate[postDate]) { + postsByDate[postDate] = []; + } + postsByDate[postDate].push(post); + }); + } - const renderCalendarCell = (date: Date, i: number, size: "sm" | "md" | "lg") => { - const dateStr = date.toDateString(); - const dayPosts = postsByDate[dateStr] || []; - const isToday = date.toDateString() === new Date(2025, 8, 20).toDateString(); - const isSelected = date.toDateString() === selectedDate.toDateString(); - const isCurrentMonth = date.getMonth() === currentDate.getMonth(); + const renderCalendarCell = ( + date: Date, + i: number, + size: "sm" | "md" | "lg", + ) => { + const dateStr = date.toDateString(); + const dayPosts = postsByDate[dateStr] || []; + const isToday = + date.toDateString() === new Date(2025, 8, 20).toDateString(); + const isSelected = date.toDateString() === selectedDate.toDateString(); + const isCurrentMonth = date.getMonth() === currentDate.getMonth(); - const cellHeightClass = size === "lg" ? "h-28" : size === "md" ? "h-24" : "h-16"; + const cellHeightClass = + size === "lg" ? "h-28" : size === "md" ? "h-24" : "h-16"; - const postsToShow = size === "lg" ? 3 : size === "md" ? 2 : 1; + const postsToShow = size === "lg" ? 3 : size === "md" ? 2 : 1; - return ( -
handleDateSelect(date)} - className={cn( - `${cellHeightClass} border border-default-200 p-1 + return ( +
handleDateSelect(date)} + className={cn( + `${cellHeightClass} border border-default-200 p-1 ${isCurrentMonth ? "bg-background" : "bg-default-50"} ${isToday ? "bg-primary/20" : ""} ${isSelected ? "border-primary border-2" : ""} cursor-pointer hover:border-primary transition-colors`, - "flex flex-col justify-between" - )} - > -
-
- {date.getDate()} -
- - {dayPosts.length > 0 && ( -
- {dayPosts.slice(0, postsToShow).map((post, i) => ( -
{ - e.stopPropagation(); - openPostEditModal(post); - }} - className={`cursor-pointer bg-primary/10 ${size === "sm" ? "h-1.5 w-full" : "text-xs py-0.5 px-1 rounded flex items-center gap-1 truncate"}`} - > - {size !== "sm" && ( - <> -
- - {new Date(post.time).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} - - - )} -
- ))} + "flex flex-col justify-between", + )} + > +
+
+ + {date.getDate()} + +
- {dayPosts.length > postsToShow && size !== "sm" &&
+{dayPosts.length - postsToShow} more
} + {dayPosts.length > 0 && ( +
+ {dayPosts.slice(0, postsToShow).map((post, i) => ( +
{ + e.stopPropagation(); + openPostEditModal(post); + }} + className={`cursor-pointer bg-primary/10 ${size === "sm" ? "h-1.5 w-full" : "text-xs py-0.5 px-1 rounded flex items-center gap-1 truncate"}`} + > + {size !== "sm" && ( + <> +
+ + {new Date( + post.time, + ).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + + + )} +
+ ))} + + {dayPosts.length > postsToShow && size !== "sm" && ( +
+ +{dayPosts.length - postsToShow} more +
+ )} +
+ )} +
+
+ {dayPosts.length > 0 && ( + + {dayPosts.length} + + )} +
- )} -
-
- {dayPosts.length > 0 && ( - - {dayPosts.length} - - )} -
-
- ); - }; + ); + }; - if (isLoading) { + if (isLoading) { + return ( +
+ +
+ ); + } return ( -
- -
- ); - } - return ( -
-
-

- {currentDate.toLocaleDateString("en-US", { - month: "long", - year: "numeric", - })} -

-
-
- -
-
- - -
-
-
- -
-
- {["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].map((day) => ( -
- {day} +
+
+

+ {currentDate.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + })} +

+
+
+ +
+
+ + +
+
- ))} - {Array.from({ length: 35 }, (_, i) => { - // Calculate the date for this cell - const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); - const dayOffset = i - firstDayOfMonth.getDay(); - const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), dayOffset + 1); +
+
+ {[ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ].map((day) => ( +
+ {day} +
+ ))} - return renderCalendarCell(date, i, "lg"); - })} -
-
+ {Array.from({ length: 35 }, (_, i) => { + // Calculate the date for this cell + const firstDayOfMonth = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + 1, + ); + const dayOffset = i - firstDayOfMonth.getDay(); + const date = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + dayOffset + 1, + ); -
-
- {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => ( -
- {day} + return renderCalendarCell(date, i, "lg"); + })} +
- ))} - {Array.from({ length: 35 }, (_, i) => { - // Calculate the date for this cell - const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); - const dayOffset = i - firstDayOfMonth.getDay(); - const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1 + dayOffset); +
+
+ {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map( + (day) => ( +
+ {day} +
+ ), + )} - return renderCalendarCell(date, i, "md"); - })} -
-
+ {Array.from({ length: 35 }, (_, i) => { + // Calculate the date for this cell + const firstDayOfMonth = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + 1, + ); + const dayOffset = i - firstDayOfMonth.getDay(); + const date = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + 1 + dayOffset, + ); -
-
- {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => ( -
- {day} + return renderCalendarCell(date, i, "md"); + })} +
- ))} - {Array.from({ length: 35 }, (_, i) => { - // Calculate the date for this cell - const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); - const dayOffset = i - firstDayOfMonth.getDay(); - const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1 + dayOffset); +
+
+ {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => ( +
+ {day} +
+ ))} - return renderCalendarCell(date, i, "sm"); - })} + {Array.from({ length: 35 }, (_, i) => { + // Calculate the date for this cell + const firstDayOfMonth = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + 1, + ); + const dayOffset = i - firstDayOfMonth.getDay(); + const date = new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + 1 + dayOffset, + ); + + return renderCalendarCell(date, i, "sm"); + })} +
+
+ + + setIsPostEditModalOpen(isOpen) + } + openPostEditModal={openPostEditModal} + closePostEditModal={() => { + setIsPostEditModalOpen(false); + setSelectedPost(null); + }} + selectedPost={selectedPost} + selectedDate={selectedDate} + posts={getPostsForSelectedDate()} + />
-
- - setIsPostEditModalOpen(isOpen)} - openPostEditModal={openPostEditModal} - closePostEditModal={() => { - setIsPostEditModalOpen(false); - setSelectedPost(null); - }} - selectedPost={selectedPost} - selectedDate={selectedDate} - posts={getPostsForSelectedDate()} - /> -
- ); + ); }; export default CalendarView; diff --git a/src/components/schedules/ImageSelectionZone.tsx b/src/components/schedules/ImageSelectionZone.tsx index 3215b86..98f3d62 100644 --- a/src/components/schedules/ImageSelectionZone.tsx +++ b/src/components/schedules/ImageSelectionZone.tsx @@ -7,329 +7,342 @@ import { Input } from "@heroui/input"; import { Spinner } from "@heroui/spinner"; import { Tab, Tabs } from "@heroui/tabs"; import { - ArrowLeft, - ArrowRight, - Search, - SearchIcon, - Trash2, + ArrowLeft, + ArrowRight, + Search, + SearchIcon, + Trash2, } from "lucide-react"; import React, { useCallback, useState } from "react"; import { FileUploadZone } from "../gallery"; import { Button } from "@heroui/button"; import { useGalleryAssets } from "@/hooks/data/use-assets"; +import Image from "next/image"; interface ImageSelectionZone { - currentImageUrl: string | null; - onImageUploaded: (imageUrl: string) => void; - onImageRemoved: () => void; - isDisabled?: boolean; + currentImageUrl: string | null; + onImageUploaded: (imageUrl: string) => void; + onImageRemoved: () => void; + isDisabled?: boolean; } export default function ImageSelectionZone({ - currentImageUrl, - onImageUploaded, - onImageRemoved, - isDisabled = false, + currentImageUrl, + onImageUploaded, + onImageRemoved, + isDisabled = false, }: ImageSelectionZone) { - const [activeSource, setActiveSource] = useState( - AssetSource.NONE - ); - const [unsplashQuery, setUnsplashQuery] = useState(""); - const [galleryQuery, setGalleryQuery] = useState(""); - const [searchQuery, setSearchQuery] = useState("technology"); - const [page, setPage] = useState(1); + const [activeSource, setActiveSource] = useState( + AssetSource.NONE, + ); + const [unsplashQuery, setUnsplashQuery] = useState(""); + const [galleryQuery, setGalleryQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState("technology"); + const [page, setPage] = useState(1); - const { data: unsplashImages, isLoading: isUnsplashLoading } = - useUnsplashImages({ query: searchQuery, page, pageSize: 6 }, true); + const { data: unsplashImages, isLoading: isUnsplashLoading } = + useUnsplashImages({ query: searchQuery, page, pageSize: 6 }, true); - const { data: galleryImages, isLoading: isGalleryLoading } = useGalleryAssets( - "default", - { - page, - pageSize: 6, - } - ); + const { data: galleryImages, isLoading: isGalleryLoading } = + useGalleryAssets("default", { + page, + pageSize: 6, + }); - // Select an Unsplash image - const handleUnsplashImageSelect = useCallback( - (url: string) => { - if (isDisabled) return; - onImageUploaded(url); - }, - [isDisabled, onImageUploaded] - ); + // Select an Unsplash image + const handleUnsplashImageSelect = useCallback( + (url: string) => { + if (isDisabled) return; + onImageUploaded(url); + }, + [isDisabled, onImageUploaded], + ); - // Handle tab change - const handleTabChange = useCallback((key: React.Key) => { - setActiveSource(key as AssetSource); - setPage(1); - }, []); + // Handle tab change + const handleTabChange = useCallback((key: React.Key) => { + setActiveSource(key as AssetSource); + setPage(1); + }, []); - // Handle Unsplash search - const handleUnsplashSearch = useCallback(async () => { - setSearchQuery(unsplashQuery.trim()); // Update the search query to trigger the API call - }, [unsplashQuery]); + // Handle Unsplash search + const handleUnsplashSearch = useCallback(async () => { + setSearchQuery(unsplashQuery.trim()); // Update the search query to trigger the API call + }, [unsplashQuery]); - const handleGallerySearch = useCallback(async () => { - setSearchQuery(galleryQuery.trim()); // Update the search query to trigger the API call - }, [galleryQuery]); + const handleGallerySearch = useCallback(async () => { + setSearchQuery(galleryQuery.trim()); // Update the search query to trigger the API call + }, [galleryQuery]); - const filteredGalleryImages = - galleryImages?.assets.filter((asset) => - asset.title.toLowerCase().includes(galleryQuery.toLowerCase()) - ) || []; + const filteredGalleryImages = + galleryImages?.assets.filter((asset) => + asset.title.toLowerCase().includes(galleryQuery.toLowerCase()), + ) || []; - // Render current image - const renderCurrentImage = () => { - if (!currentImageUrl) return null; + // Render current image + const renderCurrentImage = () => { + if (!currentImageUrl) return null; - return ( -
- Selected media - {!isDisabled && ( - - )} -
- ); - }; + return ( +
+ Selected media + {!isDisabled && ( + + )} +
+ ); + }; - // Render gallery tab content - const renderGalleryTab = () => { - if (isGalleryLoading) { - return ( -
- - Loading images... -
- ); - } + // Render gallery tab content + const renderGalleryTab = () => { + if (isGalleryLoading) { + return ( +
+ + Loading images... +
+ ); + } - if (galleryImages && galleryImages.assets.length === 0) { - return ( - - ); - } + if (galleryImages && galleryImages.assets.length === 0) { + return ( + + ); + } - return ( -
-
- setGalleryQuery(e.target.value)} - placeholder="Search in gallery" - className="flex-1" - variant="faded" - startContent={} - isDisabled={isGalleryLoading || isDisabled} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleGallerySearch(); - } - }} - /> - - -
+ return ( +
+
+ setGalleryQuery(e.target.value)} + placeholder="Search in gallery" + className="flex-1" + variant="faded" + startContent={} + isDisabled={isGalleryLoading || isDisabled} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleGallerySearch(); + } + }} + /> + + +
-
- {galleryImages && - filteredGalleryImages.map((asset) => ( - handleUnsplashImageSelect(asset.url)} - > - {asset.title - - ))} +
+ {galleryImages && + filteredGalleryImages.map((asset) => ( + + handleUnsplashImageSelect(asset.url) + } + > + {asset.title + + ))} + + {galleryImages && galleryImages.assets.length === 0 && ( +
+ No images found. Upload some images to get started. +
+ )} +
- {galleryImages && galleryImages.assets.length === 0 && ( -
- No images found. Upload some images to get started. + {galleryImages && galleryImages.totalPages > 1 && ( +
+ + +
+ )}
- )} -
+ ); + }; - {galleryImages && galleryImages.totalPages > 1 && ( -
- - -
- )} -
- ); - }; + const renderUnsplashTab = () => { + return ( +
+
+ setUnsplashQuery(e.target.value)} + placeholder="Search Unsplash images" + className="flex-1" + variant="faded" + startContent={} + isDisabled={isUnsplashLoading || isDisabled} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleUnsplashSearch(); + } + }} + /> + +
- const renderUnsplashTab = () => { - return ( -
-
- setUnsplashQuery(e.target.value)} - placeholder="Search Unsplash images" - className="flex-1" - variant="faded" - startContent={} - isDisabled={isUnsplashLoading || isDisabled} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleUnsplashSearch(); - } - }} - /> - -
+ {isUnsplashLoading ? ( +
+ + Loading images... +
+ ) : ( +
+ {unsplashImages && + unsplashImages.map((image) => ( + + handleUnsplashImageSelect( + image.urls.regular, + ) + } + > + {"Unsplash + + ))} - {isUnsplashLoading ? ( -
- - Loading images... -
- ) : ( -
- {unsplashImages && - unsplashImages.map((image) => ( - handleUnsplashImageSelect(image.urls.regular)} - > - {"Unsplash - - ))} + {unsplashImages && unsplashImages.length === 0 && ( +
+ {searchQuery + ? `No images found for "${searchQuery}". Try a different query.` + : "Enter a search term to find images."} +
+ )} +
+ )} - {unsplashImages && unsplashImages.length === 0 && ( -
- {searchQuery - ? `No images found for "${searchQuery}". Try a different query.` - : "Enter a search term to find images."} -
- )} -
- )} +
+ + +
+
+ ); + }; -
- - -
-
- ); - }; + // Render "none" tab content + const renderNoneTab = () => { + return ( +
+ No image will be used for this post. +
+ ); + }; - // Render "none" tab content - const renderNoneTab = () => { return ( -
- No image will be used for this post. -
- ); - }; - - return ( -
- {currentImageUrl ? ( - renderCurrentImage() - ) : (
- - - {renderGalleryTab()} - - - {renderUnsplashTab()} - - onImageRemoved()} - > - {renderNoneTab()} - - + {currentImageUrl ? ( + renderCurrentImage() + ) : ( +
+ + + {renderGalleryTab()} + + + {renderUnsplashTab()} + + onImageRemoved()} + > + {renderNoneTab()} + + +
+ )}
- )} -
- ); + ); } diff --git a/src/components/schedules/PostList.tsx b/src/components/schedules/PostList.tsx index f076b7b..35c291a 100644 --- a/src/components/schedules/PostList.tsx +++ b/src/components/schedules/PostList.tsx @@ -8,92 +8,93 @@ import EditPostModal from "./modal/EditPostModal"; import { addToast } from "@heroui/toast"; interface PostListProps { - selectedDate: Date; - posts: SchedulePost[]; - openPostEditModal: (post: SchedulePost) => void; - isPostEditModalOpen: boolean; - setIsPostEditModalOpen: (isOpen: boolean) => void; - closePostEditModal: () => void; - selectedPost: SchedulePost | null; + selectedDate: Date; + posts: SchedulePost[]; + openPostEditModal: (post: SchedulePost) => void; + isPostEditModalOpen: boolean; + setIsPostEditModalOpen: (isOpen: boolean) => void; + closePostEditModal: () => void; + selectedPost: SchedulePost | null; } const PostList: React.FC = ({ - openPostEditModal, - selectedDate, - posts, - isPostEditModalOpen, - selectedPost, - closePostEditModal, + openPostEditModal, + selectedDate, + posts, + isPostEditModalOpen, + selectedPost, + closePostEditModal, }) => { - const handleCreateNewPost = () => { - addToast({ - title: "This feature is coming soon!", - color: "primary", - description: "You will be able to create new posts soon.", - }); - }; + const handleCreateNewPost = () => { + addToast({ + title: "This feature is coming soon!", + color: "primary", + description: "You will be able to create new posts soon.", + }); + }; - return ( -
-
-

- Posts for{" "} - {selectedDate.toLocaleDateString("en-US", { - weekday: "long", - month: "long", - day: "numeric", - })} -

- -
+ return ( +
+
+

+ Posts for{" "} + {selectedDate.toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + })} +

+ +
-
- {posts.length > 0 ? ( - posts.map((post, i) => ( -
- - +
+ {posts.length > 0 ? ( + posts.map((post, i) => ( +
+ + +
+ )) + ) : ( +
+ +

+ No posts scheduled for this date +

+ +
+ )}
- )) - ) : ( -
- -

No posts scheduled for this date

- -
- )} -
- { - closePostEditModal(); - }} - post={selectedPost} - /> -
- ); + { + closePostEditModal(); + }} + post={selectedPost} + /> +
+ ); }; export default PostList; diff --git a/src/components/schedules/PostListItem.tsx b/src/components/schedules/PostListItem.tsx index 0bb1f8a..5781654 100644 --- a/src/components/schedules/PostListItem.tsx +++ b/src/components/schedules/PostListItem.tsx @@ -5,184 +5,192 @@ import { Chip } from "@heroui/chip"; import { SchedulePost } from "@/hooks/data/use-schedules"; import { getPlatform } from "@/lib/icons"; import { - CheckIcon, - EditIcon, - FileEditIcon, - Clock, - Loader, - CheckCircle, - AlertTriangle, - XCircle, + CheckIcon, + EditIcon, + FileEditIcon, + Clock, + Loader, + CheckCircle, + AlertTriangle, + XCircle, } from "lucide-react"; import { cn } from "@/lib/utils"; +import Image from "next/image"; interface PostListItemProps { - post: SchedulePost; - openPostEditModal: (post: SchedulePost) => void; + post: SchedulePost; + openPostEditModal: (post: SchedulePost) => void; } // Format date for display const formatDate = (date: Date) => { - return new Date(date).toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); + return new Date(date).toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); }; const getStatusInfo = (status: string) => { - const statusMap: Record< - string, - { color: string; icon: React.ReactNode; label: string } - > = { - DRAFTING: { - color: "secondary", - icon: , - label: "Drafting", - }, - DRAFT: { - color: "primary", - icon: , - label: "Draft", - }, - READY: { - color: "success", - icon: , - label: "Ready", - }, - SCHEDULED: { - color: "warning", - icon: , - label: "Scheduled", - }, - PUBLISHING: { - color: "warning", - icon: , - label: "Publishing", - }, - PUBLISHED: { - color: "success", - icon: , - label: "Published", - }, - FAILED: { - color: "danger", - icon: , - label: "Failed", - }, - CANCELED: { - color: "default", - icon: , - label: "Canceled", - }, - }; + const statusMap: Record< + string, + { + color: + | "primary" + | "default" + | "secondary" + | "success" + | "warning" + | "danger"; + icon: React.ReactNode; + label: string; + } + > = { + DRAFTING: { + color: "secondary", + icon: , + label: "Drafting", + }, + DRAFT: { + color: "primary", + icon: , + label: "Draft", + }, + READY: { + color: "success", + icon: , + label: "Ready", + }, + SCHEDULED: { + color: "warning", + icon: , + label: "Scheduled", + }, + PUBLISHING: { + color: "warning", + icon: , + label: "Publishing", + }, + PUBLISHED: { + color: "success", + icon: , + label: "Published", + }, + FAILED: { + color: "danger", + icon: , + label: "Failed", + }, + CANCELED: { + color: "default", + icon: , + label: "Canceled", + }, + }; - return statusMap[status] || { color: "default", icon: null, label: status }; + return statusMap[status] || { color: "default", icon: null, label: status }; }; const PostListItem: React.FC = ({ - post, - openPostEditModal, + post, + openPostEditModal, }) => { - const { color, icon, label } = getStatusInfo(post.status); - const isEditable = ["DRAFTING", "DRAFT", "READY"].includes(post.status); + const { color, label } = getStatusInfo(post.status); + const isEditable = ["DRAFTING", "DRAFT", "READY", "PUBLISHED"].includes( + post.status, + ); - return ( - isEditable && openPostEditModal(post)} - shadow="none" - className={cn( - "w-full", - isEditable ? "hover:border-primary cursor-pointer" : "opacity-90" - )} - > -
-
-
- - {label} - - - {formatDate(post.time)} - -
-
- {React.createElement(getPlatform(post.platform).icon)} - {post.platform} -
-
- -
- {post.imageUrl && ( -
- Post media -
- )} -
- {post.status === "DRAFTING" && post.text === "" ? ( -

- Click to add content to this post -

- ) : ( -

- {post.text} -

- )} - {post.isAIgenerated && ( - - AI Generated - + return ( + isEditable && openPostEditModal(post)} + shadow="none" + className={cn( + "w-full", + isEditable + ? "hover:border-primary cursor-pointer" + : "opacity-90", )} -
-
+ > +
+
+
+ + {label} + + + {formatDate(post.time)} + +
+
+ {React.createElement(getPlatform(post.platform).icon)} + + {post.platform} + +
+
- {post.errorMessage && ( -
- -
- )} +
+ {post.imageUrl && ( +
+ Post media +
+ )} +
+ {post.status === "DRAFTING" && post.text === "" ? ( +

+ Click to add content to this post +

+ ) : ( +

+ {post.text} +

+ )} + {post.isAIgenerated && ( + + AI Generated + + )} +
+
- {!isEditable && post.status !== "PUBLISHED" && ( -
- {post.status === "SCHEDULED" && - "This post is scheduled and cannot be edited"} - {post.status === "PUBLISHING" && - "This post is currently being published"} - {post.status === "FAILED" && "This post failed to publish"} - {post.status === "CANCELED" && "This post has been canceled"} -
- )} -
- - ); + {post.errorMessage && ( +
+ +
+ )} + + {!isEditable && post.status !== "PUBLISHED" && ( +
+ {post.status === "SCHEDULED" && + "This post is scheduled and cannot be edited"} + {post.status === "PUBLISHING" && + "This post is currently being published"} + {post.status === "FAILED" && + "This post failed to publish"} + {post.status === "CANCELED" && + "This post has been canceled"} +
+ )} +
+
+ ); }; export default PostListItem; diff --git a/src/components/schedules/forms/CreateScheduleForm.tsx b/src/components/schedules/forms/CreateScheduleForm.tsx index 6cfca18..1eb7bbf 100644 --- a/src/components/schedules/forms/CreateScheduleForm.tsx +++ b/src/components/schedules/forms/CreateScheduleForm.tsx @@ -1,8 +1,16 @@ +"use client"; + import React from "react"; -import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal"; +import { + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, +} from "@heroui/modal"; import { Select, SelectItem } from "@heroui/select"; -import { Input } from "@heroui/input"; +import { Input, Textarea } from "@heroui/input"; import { DateRangePicker } from "@heroui/date-picker"; import { parseDate } from "@internationalized/date"; @@ -21,6 +29,8 @@ import { addToast } from "@heroui/toast"; import { AssetSource } from "@/hooks/data/use-schedules"; import { timezones } from "@/lib/timezones"; import { authClient } from "@/lib/auth-client"; +import { Chip } from "@heroui/chip"; +import { ArrowLeftIcon, ArrowRightIcon, PlusIcon, X } from "lucide-react"; export interface ScheduleFormModalProps { isOpen: boolean; @@ -36,10 +46,22 @@ const scheduleSchema = z title: z.string().min(1, "Schedule name is required"), platform: z.string().min(1, "Platform is required"), timezone: z.string(), + startDate: z.date(), endDate: z.date(), - postTimes: z.array(z.instanceof(Time)).nonempty("At least one time is required"), - assetSource: z.nativeEnum(AssetSource), + + postTimes: z + .array(z.instanceof(Time)) + .nonempty("At least one time is required"), + + assetSource: z.enum(AssetSource), + unsplashTags: z.array(z.string().optional()), + + postType: z.enum(["IMAGE", "TEXT"], { + error: "Post type is required", + }), + + prompt: z.string().optional(), }) .refine((data) => data.endDate >= data.startDate, { message: "End date must be after start date", @@ -48,13 +70,33 @@ const scheduleSchema = z type ScheduleFormValues = z.infer; -export function ScheduleFormModal({ isOpen, onOpenChange }: ScheduleFormModalProps) { +const stepFormConfig = { + 1: ["title", "platform"] as const, + 2: ["postType", "assetSource", "unsplashTags", "prompt"] as const, + 3: ["startDate", "endDate", "timezone"] as const, + 4: ["postTimes"] as const, +}; + +export function ScheduleFormModal({ + isOpen, + onOpenChange, +}: ScheduleFormModalProps) { const { data: providersData } = useProviders(); const { data: user } = authClient.useSession(); + const [steps, setSteps] = React.useState(1); + const totalSteps = Object.keys(stepFormConfig).length; + const [isLoading, setIsLoading] = React.useState(false); const router = useRouter(); - const connectedProviders = Object.entries(REQUIRED_PROVIDER_CONNECTION).filter(([key]) => providersData?.some((p) => p.provider.toLowerCase() === key)); + const connectedProviders = Object.entries( + REQUIRED_PROVIDER_CONNECTION + ).filter(([key]) => + providersData?.some( + (p) => + p.provider.toLowerCase() === key && p.provider.toLowerCase() != "google" + ) + ); const availablePlatforms = connectedProviders.map(([key, provider]) => ({ value: key.toUpperCase(), @@ -67,12 +109,13 @@ export function ScheduleFormModal({ isOpen, onOpenChange }: ScheduleFormModalPro resolver: zodResolver(scheduleSchema), defaultValues: { title: "", - platform: "", - timezone: (user?.user as any)?.settings?.timezone || "UTC", startDate: new Date(), + timezone: (user?.user as any)?.settings?.timezone || "UTC", endDate: new Date(new Date().setDate(new Date().getDate() + 7)), postTimes: [new Time(9, 0, 0)], - assetSource: AssetSource.UNSPLASH, + assetSource: AssetSource.NONE, + unsplashTags: ["technology"], + prompt: "", }, mode: "onChange", }); @@ -83,8 +126,47 @@ export function ScheduleFormModal({ isOpen, onOpenChange }: ScheduleFormModalPro formState: { errors }, } = form; + const isNextStepDisabled = () => { + const live = form.watch(); + + const currentStepFields = stepFormConfig[steps]; + const optionalFields = ["unsplashTags", "prompt"]; + + // check if current includes optional fields + if (currentStepFields.some((field) => optionalFields.includes(field))) { + // If any of the optional fields are present, only require the non-optional fields to be filled + return !currentStepFields.every( + (field) => + optionalFields.includes(field) || + (!errors[field] && live[field] && live[field] !== "") + ); + } + + // If all fields in the current step are optional, enable Next button + console.log("Current Step Fields:", currentStepFields); + console.log("Live Values:", live); + + // parse and check of details are filled and optional + return !currentStepFields.every( + (field) => !errors[field] && live[field] && live[field] !== "" + ); + }; + const onSubmit = async (data: ScheduleFormValues) => { try { + if (steps < totalSteps) { + setSteps((steps + 1) as keyof typeof stepFormConfig); + return; + } + + if (connectedProviders.length === 0) { + addToast({ + title: "Please connect at least one platform", + color: "danger", + }); + return; + } + setIsLoading(true); const scheduleId = await fetcher("POST", "/schedules", { @@ -110,163 +192,555 @@ export function ScheduleFormModal({ isOpen, onOpenChange }: ScheduleFormModalPro } }; - return ( - -
- - {(onClose) => ( - <> - -

Create New Schedule

-
- -
-
- } /> -
- + const stepComponents: { [key: number]: React.ReactNode } = { + 1: ( +
+ ( + + )} + /> + ( + + )} + /> +
+

+ You have connected {connectedProviders.length} platform + {connectedProviders.length > 1 ? "s" : ""}. +

+ {connectedProviders.length === 0 && ( +

+ Please connect at least one platform to create a schedule. +

+ )} +
+
+

Hints & Tips

+
    +
  • + Choose a descriptive schedule name to easily identify it later. +
  • +
  • Ensure your selected platform is connected and authorized.
  • +
  • You can create multiple schedules for different platforms.
  • +
+
+
+ ), + 2: ( +
+ ( + + )} + /> + {form.watch("postType") == "IMAGE" && ( + ( + + )} + /> + )} + {form.watch("assetSource") == "UNSPLASH" && ( + { + const [inputValue, setInputValue] = React.useState(""); + const MAX_TAGS = 5; + if (field.value && field.value.length >= MAX_TAGS) { + return (
- ( - - )} - /> + {tag} + + ))} +
+

+ You have reached the maximum number of tags. +

+ ); + } + return ( +
+ setInputValue(e.target.value)} + isRequired + variant="bordered" + endContent={ + + } + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + const value = inputValue.trim(); + if (value && !field.value.includes(value)) { + field.onChange([...field.value, value]); + setInputValue(""); + } + } + }} + /> -
- ( - ( -
- { - if (range?.start && range?.end) { - field.onChange(range.start.toDate(form.getValues("timezone"))); - endField.onChange(range.end.toDate(form.getValues("timezone"))); - } - }} - className="w-full" - /> - {errors.endDate &&

{errors.endDate.message}

} -
- )} - /> - )} - /> +
+ {field.value.map((tag, index) => ( + { + const newTags = [...field.value]; + newTags.splice(index, 1); + field.onChange(newTags); + }} + > + + + } + > + {tag} + + ))}
- +
+ ); + }} + /> + )} + {form.watch("postType") === "TEXT" && ( + ( +
+