diff --git a/bun.lock b/bun.lock
index 89d7211..805c89b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -39,6 +39,7 @@
"@heroui/user": "^2.2.21",
"@hookform/resolvers": "^5.2.2",
"@internationalized/date": "^3.9.0",
+ "@reactour/tour": "^3.8.0",
"@tanstack/react-query": "^5.89.0",
"@tanstack/react-query-devtools": "^5.89.0",
"axios": "^1.12.2",
@@ -600,6 +601,16 @@
"@react-types/tooltip": ["@react-types/tooltip@3.4.20", "", { "dependencies": { "@react-types/overlays": "^3.9.1", "@react-types/shared": "^3.32.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-tF1yThwvgSgW8Gu/CLL0p92AUldHR6szlwhwW+ewT318sQlfabMGO4xlCNFdxJYtqTpEXk2rlaVrBuaC//du0w=="],
+ "@reactour/mask": ["@reactour/mask@1.2.0", "", { "dependencies": { "@reactour/utils": "*" }, "peerDependencies": { "react": "16.x || 17.x || 18.x || 19.x" } }, "sha512-XLgBLWfKJybtZjNTSO5lt/SIvRlCZBadB6JfE/hO1ErqURRjYhnv+edC0Ki1haUCqMGFppWk3lwcPCjmK0xNog=="],
+
+ "@reactour/popover": ["@reactour/popover@1.3.0", "", { "dependencies": { "@reactour/utils": "*" }, "peerDependencies": { "react": "16.x || 17.x || 18.x || 19.x" } }, "sha512-YdyjSmHPvEeQEcJM4gcGFa5pI/Yf4nZGqwG4JnT+rK1SyUJBIPnm4Gkl/h7/+1g0KCFMkwNwagS3ZiXvZB7ThA=="],
+
+ "@reactour/tour": ["@reactour/tour@3.8.0", "", { "dependencies": { "@reactour/mask": "*", "@reactour/popover": "*", "@reactour/utils": "*" }, "peerDependencies": { "react": "16.x || 17.x || 18.x || 19.x" } }, "sha512-KZTFi1pAvoTVKKRdBN5+XCYxXBp4k4Ql/acZcXyPvec8VU24fkMSEeV+v8krfYQpoVcewxIu3gM6xWZZLjxi7w=="],
+
+ "@reactour/utils": ["@reactour/utils@0.6.0", "", { "dependencies": { "@rooks/use-mutation-observer": "^4.11.2", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": "16.x || 17.x || 18.x || 19.x" } }, "sha512-GqaLjQi7MJsgtAKjdiw2Eak1toFkADoLRnm1+HZpaD+yl+DkaHpC1N7JAl+kVOO5I17bWInPA+OFbXjO9Co8Qg=="],
+
+ "@rooks/use-mutation-observer": ["@rooks/use-mutation-observer@4.11.2", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A=="],
+
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="],
@@ -1404,6 +1415,8 @@
"requestidlecallback": ["requestidlecallback@0.3.0", "", {}, "sha512-TWHFkT7S9p7IxLC5A1hYmAYQx2Eb9w1skrXmQ+dS1URyvR8tenMLl4lHbqEOUnpEYxNKpkVMXUgknVpBZWXXfQ=="],
+ "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="],
+
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
diff --git a/package.json b/package.json
index 1c904e1..f89b7e7 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"@heroui/user": "^2.2.21",
"@hookform/resolvers": "^5.2.2",
"@internationalized/date": "^3.9.0",
+ "@reactour/tour": "^3.8.0",
"@tanstack/react-query": "^5.89.0",
"@tanstack/react-query-devtools": "^5.89.0",
"axios": "^1.12.2",
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 9dc55c2..63f49da 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -2,6 +2,7 @@
import { useProviders } from "@/hooks/data/use-providers";
import { Button } from "@heroui/button";
+import { useTour } from "@reactour/tour";
import Link from "next/link";
export default function Home() {
@@ -21,9 +22,7 @@ export default function Home() {
}
if (data && data.length > 0) {
- const hasLinkedIn = data.some(
- (provider) => provider.provider.toLowerCase() === "linkedin"
- );
+ const hasLinkedIn = data.some((provider) => provider.provider.toLowerCase() === "linkedin");
if (!hasLinkedIn) {
return true;
}
@@ -31,25 +30,19 @@ export default function Home() {
return false;
}
-
return (
-
+
{actionNeeded() ? (
<>
Welcome to Post0
-
- Please connect your LinkedIn account to get started.
-
+
Please connect your LinkedIn account to get started.
>
) : (
<>
Welcome Back to Post0
-
- Your LinkedIn account is connected.
-
+
Your LinkedIn account is connected.
>
)}
-
{actionNeeded() ? (
diff --git a/src/app/personalize/page.tsx b/src/app/personalize/page.tsx
new file mode 100644
index 0000000..d6fa9a1
--- /dev/null
+++ b/src/app/personalize/page.tsx
@@ -0,0 +1,381 @@
+"use client";
+
+import React, { useState } from "react";
+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 { 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 { useUser } from "@/hooks/data/use-user";
+import { useGetPresets, useCreatePreset, useUpdatePreset, useDeletePreset, useUpdateUserProfile } from "@/hooks/data/use-presets";
+import LoaderSection from "@/components/shared/loader-section";
+import { addToast } from "@heroui/toast";
+
+export default function PresetsPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+ const [isEditModalOpen, setIsEditModalOpen] = useState(false);
+ const [selectedPreset, setSelectedPreset] = useState
(null);
+ const [isEditingProfile, setIsEditingProfile] = useState(false);
+
+ 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: "",
+ });
+
+ const [profileForm, setProfileForm] = useState({
+ name: userProfile?.name || "",
+ bio: userProfile?.bio || "",
+ });
+
+ React.useEffect(() => {
+ if (userProfile) {
+ setProfileForm({
+ name: userProfile.name,
+ bio: userProfile.bio || "",
+ });
+ }
+ }, [userProfile]);
+
+ const handleCreatePreset = async () => {
+ if (presetForm.title.trim() && presetForm.description.trim()) {
+ try {
+ await createPresetMutation.mutateAsync({
+ title: presetForm.title,
+ description: presetForm.description,
+ });
+
+ addToast({
+ title: "Preset created successfully!",
+ color: "success",
+ });
+
+ setPresetForm({ title: "", description: "" });
+ setIsCreateModalOpen(false);
+ } catch (error) {
+ addToast({
+ title: "Failed to create preset. Please try again.",
+ color: "danger",
+ });
+ }
+ }
+ };
+
+ const handleEditPreset = (preset: UserPreset) => {
+ setSelectedPreset(preset);
+ setPresetForm({
+ title: preset.title,
+ description: preset.description,
+ });
+ setIsEditModalOpen(true);
+ };
+
+ const handleUpdatePreset = async () => {
+ if (selectedPreset && presetForm.title.trim() && presetForm.description.trim()) {
+ try {
+ await updatePresetMutation.mutateAsync({
+ title: presetForm.title,
+ description: presetForm.description,
+ id: selectedPreset.id,
+ });
+
+ addToast({
+ title: "Preset updated successfully!",
+ color: "success",
+ });
+
+ setIsEditModalOpen(false);
+ setSelectedPreset(null);
+ setPresetForm({ title: "", description: "" });
+ } catch (error) {
+ addToast({
+ title: "Failed to update preset. Please try again.",
+ color: "danger",
+ });
+ }
+ }
+ };
+
+ const handleDeletePreset = async (id: string) => {
+ try {
+ await deletePresetMutation.mutateAsync(id);
+
+ addToast({
+ title: "Preset deleted successfully!",
+ color: "success",
+ });
+ } catch (error) {
+ addToast({
+ title: "Failed to delete preset. Please try again.",
+ color: "danger",
+ });
+ }
+ };
+
+ const handleDuplicatePreset = (preset: UserPreset) => {
+ setPresetForm({
+ title: `${preset.title} (Copy)`,
+ description: preset.description,
+ });
+ setIsCreateModalOpen(true);
+ };
+
+ const handleUpdateProfile = async () => {
+ try {
+ await updateProfileMutation.mutateAsync({
+ name: profileForm.name,
+ bio: profileForm.bio,
+ });
+
+ addToast({
+ title: "Profile updated successfully!",
+ color: "success",
+ });
+
+ setIsEditingProfile(false);
+ } catch (error) {
+ addToast({
+ title: "Failed to update profile. Please try again.",
+ color: "danger",
+ });
+ }
+ };
+
+ 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 ;
+
+ if (!userProfile) {
+ return (
+
+
Failed to load user profile. Please try again.
+
+ );
+ }
+ return (
+
+
+
+
Presets
+
Manage your AI personalities for customized content generation
+
+
} onPress={() => setIsCreateModalOpen(true)} className="w-full sm:w-auto">
+ New Preset
+
+
+
+
+
+
+
+
+
Your Profile
+
+
+
+
+
+
+
+
+ {isEditingProfile ? (
+
+ ) : (
+
+
{userProfile.name}
+
{userProfile.email}
+
{userProfile.bio || "No bio provided"}
+
+ )}
+
+
+
+
+
+
+ } value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="flex-1" variant="bordered" />
+
+
+ {!userProfile.userPreset || userProfile.userPreset.length == 0 ? (
+
+
+
+
+
No preset yet
+
Create your first AI preset to personalize your content generation experience!
+
} onPress={() => setIsCreateModalOpen(true)}>
+ Create Your First Preset
+
+
+ ) : (
+
+ {userProfile.userPreset.map((preset) => (
+
+
+
+
+
+
+
+
+
+ } onPress={() => handleEditPreset(preset)}>
+ Edit
+
+ } onPress={() => handleDuplicatePreset(preset)}>
+ Duplicate
+
+ } onPress={() => handleDeletePreset(preset.id)}>
+ Delete
+
+
+
+
+
+
+ {preset.description}
+
+
+ ))}
+
+ )}
+
+
{
+ setIsCreateModalOpen(false);
+ setPresetForm({ title: "", description: "" });
+ }}
+ size="2xl"
+ >
+
+
+
+
+
+
+ setPresetForm({ ...presetForm, title: e.target.value })} variant="bordered" isRequired isDisabled={createPresetMutation.isPending} />
+
+
+
+
+
+
+
+
+
+ {/* Edit Preset Modal */}
+
{
+ setIsEditModalOpen(false);
+ setSelectedPreset(null);
+ setPresetForm({ title: "", description: "" });
+ }}
+ size="2xl"
+ >
+
+
+
+
+ Edit Preset
+
+
+
+
+ setPresetForm({ ...presetForm, title: e.target.value })} variant="bordered" isRequired isDisabled={updatePresetMutation.isPending} />
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/personalize/type.d.ts b/src/app/personalize/type.d.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/schedules/page.tsx b/src/app/schedules/page.tsx
index 52fcabf..1cd9d9d 100644
--- a/src/app/schedules/page.tsx
+++ b/src/app/schedules/page.tsx
@@ -1,7 +1,7 @@
"use client";
import { Button } from "@heroui/button";
import { useDisclosure } from "@heroui/modal";
-import { PlusIcon, CalendarIcon, AlertCircle, Clock } from "lucide-react";
+import { PlusIcon, CalendarIcon, AlertCircle, Clock, HelpCircle } from "lucide-react";
import React, { useState } from "react";
import { ScheduleFormModal } from "@/components/schedules/forms/CreateScheduleForm";
@@ -14,12 +14,13 @@ import { Divider } from "@heroui/divider";
import { Tabs, Tab } from "@heroui/tabs";
import { useRouter } from "next/navigation";
import { getPlatform } from "@/lib/icons";
+import { useTour } from "@reactour/tour";
export default function Schedules() {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const { data: schedules, isLoading, isError } = useSchedules();
const [selectedTab, setSelectedTab] = useState("all");
-
+ const { setIsOpen: setTourOpen, setCurrentStep } = useTour();
// Filter schedules based on search query and selected tab
const filteredSchedules = React.useMemo(() => {
if (!schedules?.schedules_list) return [];
@@ -28,44 +29,27 @@ export default function Schedules() {
// Filter by tab/status
if (selectedTab !== "all") {
- filtered = filtered.filter(
- (schedule) =>
- schedule.status.toLowerCase() === selectedTab.toLowerCase()
- );
+ filtered = filtered.filter((schedule) => schedule.status.toLowerCase() === selectedTab.toLowerCase());
}
return filtered;
}, [schedules?.schedules_list, selectedTab]);
return (
-
+
Schedules
-
- Manage your posting schedules across platforms
-
+
Manage your posting schedules across platforms
-
}
- color="primary"
- variant="flat"
- onPress={onOpen}
- >
+
} color="primary" variant="flat" onPress={onOpen}>
New Schedule
-
-
setSelectedTab(key as string)}
- color="primary"
- variant="underlined"
- className="max-w-full"
- >
+
+ setSelectedTab(key as string)} color="primary" variant="underlined" className="max-w-full">
@@ -73,27 +57,31 @@ export default function Schedules() {
-
+
+
+
);
}
-function ScheduleList({
- isError,
- isLoading,
- schedules,
-}: {
- schedules: Schedule[];
- isLoading: boolean;
- isError: boolean;
-}) {
+function ScheduleList({ isError, isLoading, schedules }: { schedules: Schedule[]; isLoading: boolean; isError: boolean }) {
const router = useRouter();
// Function to get status badge color
@@ -130,7 +118,7 @@ function ScheduleList({
};
return (
-
+
{isLoading &&
}
{isError && (
@@ -141,28 +129,20 @@ function ScheduleList({
)}
{!isLoading && !isError && schedules.length === 0 && (
-
+
-
+
No schedules found
-
- Create a new schedule to start posting content
-
+
Create a new schedule to start posting content
)}
{!isLoading && !isError && schedules.length > 0 && (
-
+
{schedules.map((schedule) => (
-
handleCardClick(schedule.id)}
- shadow="sm"
- >
+ handleCardClick(schedule.id)} shadow="sm">
@@ -174,30 +154,22 @@ function ScheduleList({
-
-
- {schedule.status}
-
+
+ {schedule.status}
-
- {formatDateRange(schedule.startDate, schedule.endDate)}
-
+
{formatDateRange(schedule.startDate, schedule.endDate)}
-
- {schedule.timesPerDay} posts per day
-
+
{schedule.timesPerDay} posts per day
-
-
- Created {new Date(schedule.createdAt).toLocaleDateString()}
-
- View Details
+
+ Created {new Date(schedule.createdAt).toLocaleDateString()}
+ View Details
))}
@@ -209,16 +181,16 @@ function ScheduleList({
function LocalSkeletion() {
return (
-
+
{[1, 2, 3].map((item) => (
-
+
@@ -226,18 +198,18 @@ function LocalSkeletion() {
-
-
+
+
))}
diff --git a/src/components/schedules/forms/CreateScheduleForm.tsx b/src/components/schedules/forms/CreateScheduleForm.tsx
index c81c5ed..6cfca18 100644
--- a/src/components/schedules/forms/CreateScheduleForm.tsx
+++ b/src/components/schedules/forms/CreateScheduleForm.tsx
@@ -1,11 +1,5 @@
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";
@@ -44,9 +38,7 @@ const scheduleSchema = z
timezone: z.string(),
startDate: z.date(),
endDate: z.date(),
- postTimes: z
- .array(z.instanceof(Time))
- .nonempty("At least one time is required"),
+ postTimes: z.array(z.instanceof(Time)).nonempty("At least one time is required"),
assetSource: z.nativeEnum(AssetSource),
})
.refine((data) => data.endDate >= data.startDate, {
@@ -56,20 +48,13 @@ const scheduleSchema = z
type ScheduleFormValues = z.infer
;
-export function ScheduleFormModal({
- isOpen,
- onOpenChange,
-}: ScheduleFormModalProps) {
+export function ScheduleFormModal({ isOpen, onOpenChange }: ScheduleFormModalProps) {
const { data: providersData } = useProviders();
const { data: user } = authClient.useSession();
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));
const availablePlatforms = connectedProviders.map(([key, provider]) => ({
value: key.toUpperCase(),
@@ -126,12 +111,7 @@ export function ScheduleFormModal({
};
return (
-
+