generated from fahricansecer/boilerplate-fe
main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 1m16s
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 1m16s
This commit is contained in:
@@ -11,7 +11,7 @@ const nextConfig: NextConfig = {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/api/backend/:path*",
|
source: "/api/backend/:path*",
|
||||||
destination: "http://localhost:3001/api/:path*",
|
destination: "http://localhost:3000/api/:path*",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Box, Table, Badge, HStack, IconButton } from "@chakra-ui/react";
|
import { Box, Table, Badge, HStack, IconButton } from "@chakra-ui/react";
|
||||||
import { LuEye, LuPencil, LuTrash2 } from "react-icons/lu";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { LuEye, LuPencil, LuTrash2, LuRefreshCw } from "react-icons/lu";
|
||||||
import { toaster } from "@/components/ui/feedback/toaster";
|
import { toaster } from "@/components/ui/feedback/toaster";
|
||||||
import { useState } from "react";
|
|
||||||
import { ContentPreviewDialog } from "./ContentPreviewDialog";
|
import { ContentPreviewDialog } from "./ContentPreviewDialog";
|
||||||
|
|
||||||
const MOCK_CONTENT = [
|
|
||||||
{ id: 1, title: "The Future of AI in Marketing", platform: "LinkedIn", status: "published", date: "2024-03-10" },
|
|
||||||
{ id: 2, title: "5 Tips for Better Sleep", platform: "Twitter", status: "draft", date: "2024-03-12" },
|
|
||||||
{ id: 3, title: "Product Launch Announcement", platform: "Instagram", status: "scheduled", date: "2024-03-15" },
|
|
||||||
{ id: 4, title: "Weekly Tech Roundup", platform: "LinkedIn", status: "review", date: "2024-03-18" },
|
|
||||||
{ id: 5, title: "Customer Success Story", platform: "Blog", status: "draft", date: "2024-03-20" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -25,8 +21,36 @@ const getStatusColor = (status: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ContentTable() {
|
export function ContentTable() {
|
||||||
|
const { data: session } = useSession();
|
||||||
const [selectedItem, setSelectedItem] = useState<any>(null);
|
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
|
const [contentList, setContentList] = useState<any[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchContent = async () => {
|
||||||
|
if (!session?.accessToken) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/backend/content', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${session.accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setContentList(Array.isArray(data) ? data : []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch content:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchContent();
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
|
||||||
const handleAction = (action: string, item: any) => {
|
const handleAction = (action: string, item: any) => {
|
||||||
if (action === 'View') {
|
if (action === 'View') {
|
||||||
@@ -56,16 +80,31 @@ export function ContentTable() {
|
|||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{MOCK_CONTENT.map((item) => (
|
{isLoading ? (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colSpan={5} textAlign="center" py={10}>
|
||||||
|
<HStack justify="center" gap={2}>
|
||||||
|
<LuRefreshCw className="animate-spin" />
|
||||||
|
<Box>Loading content...</Box>
|
||||||
|
</HStack>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
) : contentList.length === 0 ? (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colSpan={5} textAlign="center" py={10}>
|
||||||
|
No content found. Start by generating some!
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
) : contentList.map((item) => (
|
||||||
<Table.Row key={item.id}>
|
<Table.Row key={item.id}>
|
||||||
<Table.Cell fontWeight="medium">{item.title}</Table.Cell>
|
<Table.Cell fontWeight="medium">{item.title}</Table.Cell>
|
||||||
<Table.Cell>{item.platform}</Table.Cell>
|
<Table.Cell>{item.type || item.platform}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Badge colorPalette={getStatusColor(item.status)} variant="solid">
|
<Badge colorPalette={getStatusColor(item.status)} variant="solid">
|
||||||
{item.status}
|
{item.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell>{item.date}</Table.Cell>
|
<Table.Cell>{new Date(item.createdAt || item.date).toLocaleDateString()}</Table.Cell>
|
||||||
<Table.Cell textAlign="right">
|
<Table.Cell textAlign="right">
|
||||||
<HStack justify="flex-end" gap={2}>
|
<HStack justify="flex-end" gap={2}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -101,6 +140,7 @@ export function ContentTable() {
|
|||||||
</Table.Root>
|
</Table.Root>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
<ContentPreviewDialog
|
<ContentPreviewDialog
|
||||||
item={selectedItem}
|
item={selectedItem}
|
||||||
open={isPreviewOpen}
|
open={isPreviewOpen}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Box, Heading, Steps, VStack, Input, Button, Text, HStack, Textarea, Card, SimpleGrid, Badge } from "@chakra-ui/react";
|
import { Box, Heading, Steps, VStack, Input, Button, Text, HStack, Textarea, Card, SimpleGrid, Badge, Progress, Spinner } from "@chakra-ui/react";
|
||||||
import { LuSparkles, LuArrowRight, LuCheck, LuHash } from "react-icons/lu";
|
import { LuSparkles, LuArrowRight, LuCheck, LuHash, LuRefreshCw } from "react-icons/lu";
|
||||||
|
|
||||||
|
|
||||||
import { toaster } from "@/components/ui/feedback/toaster";
|
import { toaster } from "@/components/ui/feedback/toaster";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { GeneratedContentResult } from "./GeneratedContentResult";
|
import { GeneratedContentResult } from "./GeneratedContentResult";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
|
||||||
// Platform Data (can also be fetched from backend if dynamic)
|
// Platform Data (can also be fetched from backend if dynamic)
|
||||||
const PLATFORMS = [
|
const PLATFORMS = [
|
||||||
@@ -39,6 +42,10 @@ export function GenerateWizard() {
|
|||||||
const [selectedNiche, setSelectedNiche] = useState("");
|
const [selectedNiche, setSelectedNiche] = useState("");
|
||||||
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
|
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
const [generationStage, setGenerationStage] = useState("");
|
||||||
|
const [generationProgress, setGenerationProgress] = useState(0);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
const [niches, setNiches] = useState<Niche[]>([]);
|
const [niches, setNiches] = useState<Niche[]>([]);
|
||||||
const [isLoadingNiches, setIsLoadingNiches] = useState(false);
|
const [isLoadingNiches, setIsLoadingNiches] = useState(false);
|
||||||
@@ -133,19 +140,36 @@ export function GenerateWizard() {
|
|||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
|
setGeneratedBundle(null);
|
||||||
|
setGenerationProgress(10);
|
||||||
|
setGenerationStage("Researching topic and niche...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
topic,
|
topic,
|
||||||
description: trendDescription || undefined,
|
description: trendDescription || undefined,
|
||||||
keywords: trendKeywords.length > 0 ? trendKeywords : undefined,
|
keywords: trendKeywords.length > 0 ? trendKeywords : undefined,
|
||||||
niche: selectedNiche, // The service expects 'niche' as string ID
|
niche: selectedNiche,
|
||||||
platforms: selectedPlatforms,
|
platforms: selectedPlatforms,
|
||||||
includeResearch: true,
|
includeResearch: true,
|
||||||
includeHashtags: true,
|
includeHashtags: true,
|
||||||
brandVoice: "friendly-expert", // Default for now, can be added to UI
|
brandVoice: "friendly-expert",
|
||||||
count: 1
|
count: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Simulated progress stages since the backend is a single call
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
setGenerationProgress(prev => {
|
||||||
|
if (prev >= 90) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
if (prev >= 60) setGenerationStage("Finalizing content and SEO...");
|
||||||
|
else if (prev >= 30) setGenerationStage("Generating platform-specific posts...");
|
||||||
|
return prev + 5;
|
||||||
|
});
|
||||||
|
}, 800);
|
||||||
|
|
||||||
const headers: HeadersInit = { 'Content-Type': 'application/json' };
|
const headers: HeadersInit = { 'Content-Type': 'application/json' };
|
||||||
if (session?.accessToken) {
|
if (session?.accessToken) {
|
||||||
headers['Authorization'] = `Bearer ${session.accessToken}`;
|
headers['Authorization'] = `Bearer ${session.accessToken}`;
|
||||||
@@ -157,29 +181,47 @@ export function GenerateWizard() {
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
|
||||||
if (!response.ok) throw new Error("Generation failed");
|
if (!response.ok) throw new Error("Generation failed");
|
||||||
|
|
||||||
const data = await response.json();
|
setGenerationProgress(95);
|
||||||
setGeneratedBundle(data);
|
setGenerationStage("Saving to library...");
|
||||||
|
|
||||||
toaster.create({
|
const data = await response.json();
|
||||||
title: "Content Generated",
|
|
||||||
description: "Your content is ready!",
|
setGenerationProgress(100);
|
||||||
type: "success"
|
setGenerationStage("Success!");
|
||||||
});
|
|
||||||
|
// Give a small delay to show 100%
|
||||||
|
setTimeout(() => {
|
||||||
|
setGeneratedBundle(data);
|
||||||
|
setIsGenerating(false);
|
||||||
|
|
||||||
|
toaster.create({
|
||||||
|
title: "Content Generated",
|
||||||
|
description: "Your content is ready and saved to library!",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-redirect after success
|
||||||
|
if (data.masterContentId) {
|
||||||
|
router.push(`/[locale]/content?id=${data.masterContentId}`);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
setIsGenerating(false);
|
||||||
toaster.create({
|
toaster.create({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "Failed to generate content. Please try again.",
|
description: "Failed to generate content. Please try again.",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
if (generatedBundle) {
|
if (generatedBundle) {
|
||||||
return <GeneratedContentResult bundle={generatedBundle} onReset={() => {
|
return <GeneratedContentResult bundle={generatedBundle} onReset={() => {
|
||||||
setGeneratedBundle(null);
|
setGeneratedBundle(null);
|
||||||
@@ -204,7 +246,26 @@ export function GenerateWizard() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box p={6} borderWidth="1px" borderRadius="lg" bg="bg.panel">
|
<Box p={6} borderWidth="1px" borderRadius="lg" bg="bg.panel">
|
||||||
{activeStep === 0 && (
|
{isGenerating ? (
|
||||||
|
<VStack py={12} gap={6}>
|
||||||
|
<Spinner size="xl" color="blue.500" />
|
||||||
|
<VStack gap={2} width="full" maxW="md">
|
||||||
|
<Text fontWeight="bold" fontSize="lg">{generationStage}</Text>
|
||||||
|
<Progress.Root value={generationProgress} width="full" colorPalette="blue" shape="rounded" size="sm">
|
||||||
|
<Progress.Track>
|
||||||
|
<Progress.Range />
|
||||||
|
</Progress.Track>
|
||||||
|
</Progress.Root>
|
||||||
|
|
||||||
|
<Text fontSize="sm" color="fg.muted">{generationProgress}% Complete</Text>
|
||||||
|
</VStack>
|
||||||
|
<Text textAlign="center" color="fg.muted" maxW="xs">
|
||||||
|
Our AI is analyzing the topic, researching facts, and crafting perfect posts for your platforms.
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
) : activeStep === 0 && (
|
||||||
|
|
||||||
|
|
||||||
<VStack align="stretch" gap={6}>
|
<VStack align="stretch" gap={6}>
|
||||||
<Heading size="md">1. Start with a Topic</Heading>
|
<Heading size="md">1. Start with a Topic</Heading>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user