main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m44s

This commit is contained in:
Harun CAN
2026-02-10 12:28:10 +03:00
parent c5804e3b53
commit 05435cbaf8
35 changed files with 2635 additions and 261 deletions

13
.env Normal file
View File

@@ -0,0 +1,13 @@
# NextAuth Configuration
NEXTAUTH_URL=http://localhost:3001
NEXTAUTH_SECRET=dev_secret_key_change_in_production
# Backend API URL
NEXT_PUBLIC_API_URL=http://localhost:3000/api
# Auth Mode
NEXT_PUBLIC_AUTH_REQUIRED=false
# Third Party Keys (Placeholders)
NEXT_PUBLIC_GOOGLE_API_KEY='test-key'

32
messages/ar.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/de.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/es.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/fr.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/ja.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/pt.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/ru.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/zh.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -11,7 +11,7 @@ const nextConfig: NextConfig = {
return [
{
source: "/api/backend/:path*",
destination: "http://localhost:3000/api/:path*",
destination: "http://localhost:3001/api/:path*",
},
];
},

20
package-lock.json generated
View File

@@ -146,7 +146,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -556,7 +555,6 @@
"resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-3.31.0.tgz",
"integrity": "sha512-puvrZOfnfMA+DckDcz0UxO20l7TVhwsdQ9ksCv4nIUB430yuWzon0yo9fM10lEr3hd7BhjZARpMCVw5u280clw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ark-ui/react": "^5.29.1",
"@emotion/is-prop-valid": "^1.4.0",
@@ -692,7 +690,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1986,7 +1983,6 @@
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
"integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -2905,7 +2901,6 @@
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
@@ -3067,7 +3062,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3127,7 +3121,6 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
@@ -4555,7 +4548,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4921,7 +4913,6 @@
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.26.0"
}
@@ -5028,7 +5019,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5781,7 +5771,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5983,7 +5972,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7935,7 +7923,6 @@
"integrity": "sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg==",
"deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.",
"license": "MIT",
"peer": true,
"dependencies": {
"@next/env": "16.0.0",
"@swc/helpers": "0.5.15",
@@ -8599,7 +8586,6 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -8747,7 +8733,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8757,7 +8742,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -8770,7 +8754,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9683,7 +9666,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -9864,7 +9846,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10319,7 +10300,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -0,0 +1,22 @@
import { getTranslations } from "next-intl/server";
import { Box, Heading, Text, Center } from "@chakra-ui/react";
import { LuTrendingUp } from "react-icons/lu";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `Analytics | Content Hunter`,
};
}
export default function AnalyticsPage() {
return (
<Box>
<Heading size="2xl" mb={4}>Analytics & Insights</Heading>
<Center h="50vh" flexDirection="column" color="fg.muted">
<LuTrendingUp size={48} />
<Text mt={4} fontSize="lg">Analytics dashboard coming soon.</Text>
</Center>
</Box>
);
}

View File

@@ -0,0 +1,19 @@
import { getTranslations } from "next-intl/server";
import { Box, Heading } from "@chakra-ui/react";
import { ContentTable } from "@/components/content/ContentTable";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `Content | Content Hunter`,
};
}
export default function ContentPage() {
return (
<Box>
<Heading size="2xl" mb={6}>Content Management</Heading>
<ContentTable />
</Box>
);
}

View File

@@ -0,0 +1,22 @@
import { Box, Heading } from "@chakra-ui/react";
import { GenerateWizard } from "@/components/generate/GenerateWizard";
import { getTranslations } from "next-intl/server";
import { Suspense } from "react";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `Generate Content | Content Hunter`,
};
}
export default function GeneratePage() {
return (
<Box>
<Heading size="2xl" mb={6}>Content Generator</Heading>
<Suspense fallback={<Box>Loading...</Box>}>
<GenerateWizard />
</Suspense>
</Box>
);
}

View File

@@ -0,0 +1,67 @@
import { getTranslations } from "next-intl/server";
import { Box, Heading, Text, SimpleGrid, Card } from "@chakra-ui/react";
import { LuFileText, LuCalendar, LuTrendingUp } from "react-icons/lu";
import { Link } from '@/i18n/navigation';
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("home")} | Content Hunter`,
};
}
export default function Home() {
return (
<>
<Box mb={8}>
<Heading size="2xl" mb={2} fontWeight="bold">Welcome back, Hunter!</Heading>
<Text color="fg.muted" fontSize="lg">Here's what's happening with your content today.</Text>
</Box>
<SimpleGrid columns={{ base: 1, md: 3 }} gap={6}>
<Link href="/content">
<Card.Root _hover={{ transform: 'translateY(-4px)', shadow: 'md', transition: 'all 0.2s' }}>
<Card.Body>
<Box mb={4} color="primary.solid">
<LuFileText size={24} />
</Box>
<Card.Title mb={2}>Content Pieces</Card.Title>
<Card.Description>
You have 12 active content pieces in your pipeline.
</Card.Description>
</Card.Body>
</Card.Root>
</Link>
<Link href="/schedule">
<Card.Root _hover={{ transform: 'translateY(-4px)', shadow: 'md', transition: 'all 0.2s' }}>
<Card.Body>
<Box mb={4} color="primary.solid">
<LuCalendar size={24} />
</Box>
<Card.Title mb={2}>Scheduled Posts</Card.Title>
<Card.Description>
5 posts are scheduled for meaningful engagement today.
</Card.Description>
</Card.Body>
</Card.Root>
</Link>
<Link href="/analytics">
<Card.Root _hover={{ transform: 'translateY(-4px)', shadow: 'md', transition: 'all 0.2s' }}>
<Card.Body>
<Box mb={4} color="primary.solid">
<LuTrendingUp size={24} />
</Box>
<Card.Title mb={2}>Analytics</Card.Title>
<Card.Description>
Your engagement rate increased by 15% this week.
</Card.Description>
</Card.Body>
</Card.Root>
</Link>
</SimpleGrid>
</>
);
}

View File

@@ -0,0 +1,7 @@
'use client';
import { DashboardLayout } from '@/components/layout/dashboard/DashboardLayout';
export default function Layout({ children }: { children: React.ReactNode }) {
return <DashboardLayout>{children}</DashboardLayout>;
}

View File

@@ -0,0 +1,22 @@
import { getTranslations } from "next-intl/server";
import { Box, Heading, Text, Center } from "@chakra-ui/react";
import { LuCalendar } from "react-icons/lu";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `Schedule | Content Hunter`,
};
}
export default function SchedulePage() {
return (
<Box>
<Heading size="2xl" mb={4}>Schedule & Calendar</Heading>
<Center h="50vh" flexDirection="column" color="fg.muted">
<LuCalendar size={48} />
<Text mt={4} fontSize="lg">Calendar and scheduling tools coming soon.</Text>
</Center>
</Box>
);
}

View File

@@ -0,0 +1,22 @@
import { getTranslations } from "next-intl/server";
import { Box, Heading, Text, Center } from "@chakra-ui/react";
import { LuSettings } from "react-icons/lu";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `Settings | Content Hunter`,
};
}
export default function SettingsPage() {
return (
<Box>
<Heading size="2xl" mb={4}>Settings</Heading>
<Center h="50vh" flexDirection="column" color="fg.muted">
<LuSettings size={48} />
<Text mt={4} fontSize="lg">Global application settings coming soon.</Text>
</Center>
</Box>
);
}

View File

@@ -0,0 +1,321 @@
"use client";
import { Box, Heading, VStack, Text, Card, HStack, Badge, Button, Input, SimpleGrid, Spinner, Icon, Table, IconButton, Dialog, Field } from "@chakra-ui/react";
import { LuPlus, LuTrash, LuLink, LuRefreshCw, LuSparkles, LuEye, LuTrendingUp, LuUser } from "react-icons/lu";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "@/i18n/navigation";
interface SourceAccount {
id: string;
platform: string;
username: string;
url: string;
followersCount?: number;
postsAnalyzed?: number;
averageEngagement?: number;
lastAnalyzed?: string;
status: string;
}
interface AnalyzedPost {
id: string;
content: string;
engagement: number;
likes: number;
comments: number;
postedAt: string;
viralScore?: number;
}
export default function SourceAccountsPage() {
const { data: session } = useSession();
const router = useRouter();
const [accounts, setAccounts] = useState<SourceAccount[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isAdding, setIsAdding] = useState(false);
const [showAddModal, setShowAddModal] = useState(false);
const [newAccountUrl, setNewAccountUrl] = useState("");
const [selectedAccount, setSelectedAccount] = useState<SourceAccount | null>(null);
const [posts, setPosts] = useState<AnalyzedPost[]>([]);
useEffect(() => {
fetchAccounts();
}, []);
const getHeaders = (): HeadersInit => {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
return headers;
};
const fetchAccounts = async () => {
try {
const res = await fetch('/api/backend/source-accounts', { headers: getHeaders() });
if (res.ok) {
const data = await res.json();
setAccounts(Array.isArray(data) ? data : []);
}
} catch (error) {
console.error('Fetch error:', error);
} finally {
setIsLoading(false);
}
};
const handleAddAccount = async () => {
if (!newAccountUrl.trim()) {
toaster.create({ title: "Please enter a URL", type: "warning" });
return;
}
setIsAdding(true);
try {
const res = await fetch('/api/backend/source-accounts', {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ url: newAccountUrl }),
});
if (res.ok) {
toaster.create({ title: "Source account added", type: "success" });
setNewAccountUrl("");
setShowAddModal(false);
fetchAccounts();
} else {
throw new Error('Failed to add account');
}
} catch (error) {
toaster.create({ title: "Failed to add account", type: "error" });
} finally {
setIsAdding(false);
}
};
const handleDeleteAccount = async (id: string) => {
try {
const res = await fetch(`/api/backend/source-accounts/${id}`, {
method: 'DELETE',
headers: getHeaders(),
});
if (res.ok) {
toaster.create({ title: "Account removed", type: "success" });
setAccounts(accounts.filter(a => a.id !== id));
}
} catch (error) {
toaster.create({ title: "Failed to remove account", type: "error" });
}
};
const handleAnalyze = async (account: SourceAccount) => {
toaster.create({ title: "Analyzing account...", type: "info" });
try {
const res = await fetch(`/api/backend/source-accounts/${account.id}/analyze`, {
method: 'POST',
headers: getHeaders(),
});
if (res.ok) {
toaster.create({ title: "Analysis complete", type: "success" });
fetchAccounts();
}
} catch (error) {
toaster.create({ title: "Analysis failed", type: "error" });
}
};
const handleViewPosts = async (account: SourceAccount) => {
setSelectedAccount(account);
try {
const res = await fetch(`/api/backend/source-accounts/${account.id}/posts`, {
headers: getHeaders()
});
if (res.ok) {
const data = await res.json();
setPosts(Array.isArray(data) ? data : []);
}
} catch (error) {
console.error('Error fetching posts:', error);
}
};
const handleInspireContent = (post?: AnalyzedPost) => {
const topic = post?.content?.slice(0, 100) || selectedAccount?.username;
router.push(`/generate?topic=${encodeURIComponent(topic || '')}&mode=inspire`);
};
return (
<VStack align="stretch" gap={6} p={6}>
{/* Header */}
<HStack justify="space-between">
<Box>
<Heading size="xl">Kaynak Hesaplar</Heading>
<Text color="fg.muted">Başarılı hesapları takip et ve içerik ilhamı al</Text>
</Box>
<Button colorPalette="blue" onClick={() => setShowAddModal(true)}>
<LuPlus /> Hesap Ekle
</Button>
</HStack>
{/* Accounts Grid */}
{isLoading ? (
<VStack py={10}><Spinner size="lg" /></VStack>
) : accounts.length > 0 ? (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={4}>
{accounts.map((account) => (
<Card.Root key={account.id}>
<Card.Header pb={2}>
<HStack justify="space-between">
<HStack>
<Icon as={LuUser} boxSize={5} />
<Heading size="md">@{account.username}</Heading>
</HStack>
<Badge colorPalette={account.status === 'active' ? 'green' : 'gray'}>
{account.platform}
</Badge>
</HStack>
</Card.Header>
<Card.Body>
<VStack align="start" gap={3}>
<HStack w="100%" justify="space-between">
<Text fontSize="sm" color="fg.muted">
<Icon as={LuTrendingUp} /> {account.followersCount?.toLocaleString() || 0} takipçi
</Text>
<Text fontSize="sm" color="fg.muted">
{account.postsAnalyzed || 0} post analiz
</Text>
</HStack>
{account.averageEngagement && (
<Badge colorPalette="blue" variant="subtle">
Avg. Engagement: {account.averageEngagement.toFixed(1)}%
</Badge>
)}
<HStack w="100%" justify="space-between" pt={2}>
<Button size="sm" variant="outline" onClick={() => handleViewPosts(account)}>
<LuEye /> Postlar
</Button>
<Button size="sm" variant="outline" onClick={() => handleAnalyze(account)}>
<LuRefreshCw /> Analiz Et
</Button>
<IconButton
size="sm"
variant="ghost"
colorPalette="red"
onClick={() => handleDeleteAccount(account.id)}
aria-label="Delete"
>
<LuTrash />
</IconButton>
</HStack>
</VStack>
</Card.Body>
</Card.Root>
))}
</SimpleGrid>
) : (
<Card.Root>
<Card.Body>
<VStack py={10} gap={4}>
<Icon as={LuLink} boxSize={12} color="fg.muted" />
<Text color="fg.muted" textAlign="center">
Henüz kaynak hesap eklenmedi.<br />
Başarılı hesapları ekleyerek içerik ilhamı alın.
</Text>
<Button colorPalette="blue" onClick={() => setShowAddModal(true)}>
<LuPlus /> İlk Hesabı Ekle
</Button>
</VStack>
</Card.Body>
</Card.Root>
)}
{/* Posts Modal */}
{selectedAccount && (
<Dialog.Root open={!!selectedAccount} onOpenChange={() => setSelectedAccount(null)}>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content maxW="800px">
<Dialog.Header>
<Dialog.Title>@{selectedAccount.username} Postları</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<VStack gap={3} align="stretch">
{posts.map((post) => (
<Card.Root key={post.id} size="sm">
<Card.Body>
<VStack align="start" gap={2}>
<Text fontSize="sm">{post.content}</Text>
<HStack justify="space-between" w="100%">
<HStack gap={2}>
<Badge> {post.likes}</Badge>
<Badge>💬 {post.comments}</Badge>
{post.viralScore && (
<Badge colorPalette="purple">
🔥 Viral: {post.viralScore}
</Badge>
)}
</HStack>
<Button size="xs" colorPalette="green" onClick={() => handleInspireContent(post)}>
<LuSparkles /> İçerik Oluştur
</Button>
</HStack>
</VStack>
</Card.Body>
</Card.Root>
))}
{posts.length === 0 && (
<Text color="fg.muted" textAlign="center" py={4}>
Henüz analiz edilmiş post yok
</Text>
)}
</VStack>
</Dialog.Body>
<Dialog.Footer>
<Button variant="outline" onClick={() => setSelectedAccount(null)}>
Kapat
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
)}
{/* Add Account Modal */}
<Dialog.Root open={showAddModal} onOpenChange={() => setShowAddModal(false)}>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Kaynak Hesap Ekle</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<VStack gap={4}>
<Field.Root>
<Field.Label>Hesap URL'si</Field.Label>
<Input
placeholder="https://twitter.com/username veya https://instagram.com/username"
value={newAccountUrl}
onChange={(e) => setNewAccountUrl(e.target.value)}
/>
<Field.HelperText>Twitter, Instagram, LinkedIn veya TikTok hesap linki</Field.HelperText>
</Field.Root>
</VStack>
</Dialog.Body>
<Dialog.Footer>
<Button variant="outline" onClick={() => setShowAddModal(false)}>İptal</Button>
<Button colorPalette="blue" onClick={handleAddAccount} disabled={isAdding}>
{isAdding ? <Spinner size="sm" /> : <LuPlus />} Ekle
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
</VStack>
);
}

View File

@@ -0,0 +1,371 @@
"use client";
import { Box, Heading, VStack, Text, Card, HStack, Badge, Button, Input, SimpleGrid, Spinner, Icon } from "@chakra-ui/react";
import { LuTrendingUp, LuSearch, LuSparkles, LuRefreshCw, LuArrowRight, LuGlobe, LuHash, LuZoomIn } from "react-icons/lu";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "@/i18n/navigation";
interface Trend {
id: string;
title: string;
description?: string;
score: number;
volume?: number;
source: string;
keywords: string[];
relatedTopics: string[];
url?: string;
}
export default function TrendsPage() {
const { data: session } = useSession();
const router = useRouter();
const [niche, setNiche] = useState("");
const [trends, setTrends] = useState<Trend[]>([]);
const [isScanning, setIsScanning] = useState(false);
const [isDeepScanning, setIsDeepScanning] = useState(false);
const [selectedTrend, setSelectedTrend] = useState<Trend | null>(null);
const [scanCount, setScanCount] = useState(0);
const [translatingId, setTranslatingId] = useState<string | null>(null);
const [translatedContent, setTranslatedContent] = useState<Record<string, { title: string; description?: string }>>({});
const handleScanTrends = async () => {
if (!niche.trim()) {
toaster.create({ title: "Please enter a niche or topic", type: "warning" });
return;
}
setIsScanning(true);
try {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
const res = await fetch('/api/backend/trends/scan', {
method: 'POST',
headers,
body: JSON.stringify({
keywords: [niche],
country: 'TR',
}),
});
if (res.ok) {
const response = await res.json();
// Backend wraps response in { success, status, message, data }
const trendsData = response.data || response.trends || response;
const trendsArray = Array.isArray(trendsData) ? trendsData : [];
setTrends(trendsArray);
setScanCount(1);
toaster.create({
title: `${trendsArray.length} trend bulundu`,
type: trendsArray.length > 0 ? "success" : "info"
});
} else {
throw new Error('Failed to scan trends');
}
} catch (error) {
console.error('Scan error:', error);
toaster.create({ title: "Failed to scan trends", type: "error" });
} finally {
setIsScanning(false);
}
};
// DEEPER RESEARCH - Prepends new results and uses all languages
const handleDeepResearch = async () => {
if (!niche.trim()) {
toaster.create({ title: "Please enter a niche or topic first", type: "warning" });
return;
}
setIsDeepScanning(true);
try {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
// Use different keywords for deeper research
const deepKeywords = [
niche,
`${niche} trends`,
`${niche} news`,
`latest ${niche}`,
];
const res = await fetch('/api/backend/trends/scan', {
method: 'POST',
headers,
body: JSON.stringify({
keywords: deepKeywords,
country: 'TR',
allLanguages: true, // Fetch global trends
}),
});
if (res.ok) {
const response = await res.json();
const trendsData = response.data || response.trends || response;
const newTrends = Array.isArray(trendsData) ? trendsData : [];
// Prepend new trends, filter duplicates by title
const existingTitles = new Set(trends.map(t => t.title.toLowerCase()));
const uniqueNewTrends = newTrends.filter(
(t: Trend) => !existingTitles.has(t.title.toLowerCase())
);
setTrends(prev => [...uniqueNewTrends, ...prev]);
setScanCount(prev => prev + 1);
toaster.create({
title: `${uniqueNewTrends.length} yeni trend eklendi (Üste eklendi)`,
description: `Toplam: ${trends.length + uniqueNewTrends.length} trend`,
type: uniqueNewTrends.length > 0 ? "success" : "info"
});
} else {
throw new Error('Failed to deep scan');
}
} catch (error) {
console.error('Deep scan error:', error);
toaster.create({ title: "Derin araştırma başarısız", type: "error" });
} finally {
setIsDeepScanning(false);
}
};
const handleTranslate = async (trend: Trend) => {
if (translatedContent[trend.id]) return;
setTranslatingId(trend.id);
try {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
// Translate title and description together for efficiency
const textToTranslate = `${trend.title}\n---\n${trend.description || ""}`;
const res = await fetch('/api/backend/trends/translate', {
method: 'POST',
headers,
body: JSON.stringify({
text: textToTranslate,
targetLanguage: 'Turkish',
}),
});
if (res.ok) {
const responseData = await res.json();
// Handle potential 'data' wrapping from NestJS interceptors
const data = responseData.data || responseData;
const translatedText = data.translatedText;
if (translatedText && typeof translatedText === 'string') {
const [newTitle, ...descParts] = translatedText.split('\n---\n');
setTranslatedContent(prev => ({
...prev,
[trend.id]: {
title: newTitle.trim(),
description: descParts.join('\n---\n').trim(),
}
}));
} else {
throw new Error('Invalid translation response');
}
}
} catch (error) {
console.error('Translation error:', error);
toaster.create({ title: "Çeviri başarısız", type: "error" });
} finally {
setTranslatingId(null);
}
};
const handleCreateContent = (trend: Trend) => {
// Navigate to generate page with pre-filled trend data
const params = new URLSearchParams();
params.set('topic', trend.title);
if (trend.description) {
params.set('description', trend.description);
}
if (trend.keywords && trend.keywords.length > 0) {
params.set('keywords', JSON.stringify(trend.keywords));
}
if (trend.source) {
params.set('source', trend.source);
}
router.push(`/generate?${params.toString()}`);
};
return (
<VStack align="stretch" gap={6} p={6}>
{/* Header */}
<HStack justify="space-between">
<Box>
<Heading size="xl">Trend Araştırması</Heading>
<Text color="fg.muted">Güncel trendleri keşfet ve içerik oluştur</Text>
</Box>
</HStack>
{/* Search Box */}
<Card.Root>
<Card.Body>
<VStack gap={4}>
<HStack w="100%" gap={4}>
<Input
placeholder="Niş veya konu girin (örn: yapay zeka, kripto, fitness)"
value={niche}
onChange={(e) => setNiche(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleScanTrends()}
flex={1}
/>
<Button
colorPalette="blue"
onClick={handleScanTrends}
disabled={isScanning}
>
{isScanning ? <Spinner size="sm" /> : <LuSearch />}
{isScanning ? "Taranıyor..." : "Trend Tara"}
</Button>
</HStack>
<Text fontSize="sm" color="fg.muted">
Google Trends, Twitter, Reddit ve haber kaynaklarından güncel trendleri tarar
</Text>
</VStack>
</Card.Body>
</Card.Root>
{/* Deeper Research Button - Shows after initial scan */}
{trends.length > 0 && (
<HStack justify="space-between" align="center">
<Text color="fg.muted">
{trends.length} trend bulundu (Tarama #{scanCount})
</Text>
<Button
colorPalette="purple"
variant="outline"
onClick={handleDeepResearch}
disabled={isDeepScanning || isScanning}
>
{isDeepScanning ? <Spinner size="sm" /> : <LuZoomIn />}
{isDeepScanning ? "Derin Araştırma Yapılıyor..." : "Daha Derin Araştır"}
</Button>
</HStack>
)}
{/* Results */}
{trends.length > 0 && (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={4}>
{trends.map((trend) => (
<Card.Root
key={trend.id}
cursor="pointer"
borderWidth={selectedTrend?.id === trend.id ? "2px" : "1px"}
borderColor={selectedTrend?.id === trend.id ? "blue.500" : "border.subtle"}
onClick={() => setSelectedTrend(trend)}
_hover={{ shadow: "md" }}
>
<Card.Header pb={2}>
<HStack justify="space-between">
<Badge colorPalette="blue" variant="subtle">
<LuTrendingUp /> {Math.round(trend.score)}
</Badge>
<Badge variant="outline">{trend.source}</Badge>
</HStack>
</Card.Header>
<Card.Body pt={0}>
<VStack align="start" gap={2}>
<Heading size="md">
{translatedContent[trend.id]?.title || trend.title}
</Heading>
{(translatedContent[trend.id]?.description || trend.description) && (
<Text fontSize="sm" color="fg.muted" lineClamp={2}>
{translatedContent[trend.id]?.description || trend.description}
</Text>
)}
{translatedContent[trend.id] && (
<Badge colorPalette="orange" size="xs" variant="outline">
AI Çeviri
</Badge>
)}
{trend.keywords.length > 0 && (
<HStack flexWrap="wrap" gap={1}>
{trend.keywords.slice(0, 3).map((kw, i) => (
<Badge key={i} size="sm" variant="subtle">
<LuHash /> {kw}
</Badge>
))}
</HStack>
)}
<HStack w="100%" justify="space-between" pt={2} flexWrap="wrap">
<HStack gap={2}>
{trend.url && (
<Button
size="sm"
variant="ghost"
paddingInline={2}
onClick={(e) => {
e.stopPropagation();
window.open(trend.url, '_blank');
}}
>
<LuGlobe /> Kaynak
</Button>
)}
{!translatedContent[trend.id] && trend.relatedTopics?.some(t => ['EN', 'DE'].includes(t)) && (
<Button
size="sm"
variant="ghost"
colorPalette="blue"
loading={translatingId === trend.id}
onClick={(e) => {
e.stopPropagation();
handleTranslate(trend);
}}
>
<LuRefreshCw /> Çevir
</Button>
)}
</HStack>
<Button
size="sm"
colorPalette="green"
onClick={(e) => {
e.stopPropagation();
handleCreateContent(trend);
}}
>
<LuSparkles /> İçerik Oluştur
</Button>
</HStack>
</VStack>
</Card.Body>
</Card.Root>
))}
</SimpleGrid>
)}
{/* Empty State */}
{!isScanning && trends.length === 0 && (
<Card.Root>
<Card.Body>
<VStack py={10} gap={4}>
<Icon as={LuTrendingUp} boxSize={12} color="fg.muted" />
<Text color="fg.muted" textAlign="center">
Henüz trend taraması yapılmadı.<br />
Bir niş veya konu girerek başlayın.
</Text>
</VStack>
</Card.Body>
</Card.Root>
)}
</VStack>
);
}

View File

@@ -0,0 +1,252 @@
"use client";
import { Box, Heading, VStack, Text, Card, HStack, Badge, Button, Input, SimpleGrid, Spinner, Icon, Tabs, Textarea } from "@chakra-ui/react";
import { LuVideo, LuImage, LuDownload, LuSparkles, LuRefreshCw, LuCopy } from "react-icons/lu";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
import { useSession } from "next-auth/react";
interface GeneratedAsset {
id: string;
type: 'image' | 'video' | 'thumbnail';
url: string;
prompt: string;
createdAt: string;
}
export default function VideoPage() {
const { data: session } = useSession();
const [topic, setTopic] = useState("");
const [style, setStyle] = useState("professional");
const [isGenerating, setIsGenerating] = useState(false);
const [generatedAssets, setGeneratedAssets] = useState<GeneratedAsset[]>([]);
const [activeTab, setActiveTab] = useState("thumbnail");
const getHeaders = (): HeadersInit => {
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
return headers;
};
const handleGenerate = async (type: 'thumbnail' | 'video') => {
if (!topic.trim()) {
toaster.create({ title: "Please enter a topic", type: "warning" });
return;
}
setIsGenerating(true);
try {
// Use correct backend endpoints
const endpoint = type === 'thumbnail'
? '/api/backend/video-thumbnail/thumbnails/generate'
: '/api/backend/video-thumbnail/package';
const body = type === 'thumbnail'
? {
title: topic,
videoType: style,
pattern: 'face_text',
keyMessage: topic,
}
: {
topic,
platform: 'youtube',
format: 'short',
targetAudience: 'general',
};
const res = await fetch(endpoint, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(body),
});
if (res.ok) {
const data = await res.json();
// Extract URL from the response - thumbnail returns prompt, not URL
const assetUrl = data.url || data.thumbnailUrl || data.videoUrl || '';
const promptData = data.prompt || data.thumbnail?.prompt || topic;
setGeneratedAssets(prev => [{
id: `asset-${Date.now()}`,
type,
url: assetUrl || `/api/placeholder/${type}`, // Fallback
prompt: typeof promptData === 'string' ? promptData : topic,
createdAt: new Date().toISOString(),
}, ...prev]);
toaster.create({ title: `${type === 'thumbnail' ? 'Thumbnail' : 'Video'} package generated!`, type: "success" });
} else {
const error = await res.json();
throw new Error(error.message || 'Generation failed');
}
} catch (error: any) {
console.error('Generation error:', error);
toaster.create({ title: error.message || "Generation failed", type: "error" });
} finally {
setIsGenerating(false);
}
};
const handleCopyPrompt = (prompt: string) => {
navigator.clipboard.writeText(prompt);
toaster.create({ title: "Prompt copied", type: "success" });
};
const handleDownload = async (url: string, filename: string) => {
try {
const response = await fetch(url);
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
toaster.create({ title: "Download failed", type: "error" });
}
};
return (
<VStack align="stretch" gap={6} p={6}>
{/* Header */}
<HStack justify="space-between">
<Box>
<Heading size="xl">Video & Thumbnail</Heading>
<Text color="fg.muted">AI ile görsel ve video oluştur</Text>
</Box>
</HStack>
{/* Generator Card */}
<Card.Root>
<Card.Body>
<Tabs.Root value={activeTab} onValueChange={(e) => setActiveTab(e.value)}>
<Tabs.List mb={4}>
<Tabs.Trigger value="thumbnail">
<LuImage /> Thumbnail
</Tabs.Trigger>
<Tabs.Trigger value="video">
<LuVideo /> Video
</Tabs.Trigger>
</Tabs.List>
<VStack gap={4} align="stretch">
<Textarea
placeholder="Konu veya başlık girin (örn: 5 Dakikada Python Öğrenin)"
value={topic}
onChange={(e) => setTopic(e.target.value)}
rows={3}
/>
<HStack>
<Box flex={1}>
<Text fontSize="sm" mb={1}>Stil</Text>
<select
value={style}
onChange={(e) => setStyle(e.target.value)}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid #ccc',
}}
>
<option value="professional">Profesyonel</option>
<option value="vibrant">Canlı Renkler</option>
<option value="minimal">Minimal</option>
<option value="bold">Dikkat Çekici</option>
<option value="cinematic">Sinematik</option>
</select>
</Box>
<Button
colorPalette="blue"
onClick={() => handleGenerate(activeTab as 'thumbnail' | 'video')}
disabled={isGenerating}
mt={6}
>
{isGenerating ? <Spinner size="sm" /> : <LuSparkles />}
{isGenerating ? "Oluşturuluyor..." : "Oluştur"}
</Button>
</HStack>
</VStack>
</Tabs.Root>
</Card.Body>
</Card.Root>
{/* Generated Assets */}
{generatedAssets.length > 0 && (
<Box>
<Heading size="md" mb={4}>Oluşturulan Görseller</Heading>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={4}>
{generatedAssets.map((asset) => (
<Card.Root key={asset.id}>
<Card.Body p={0}>
{asset.type === 'video' ? (
<video
src={asset.url}
controls
style={{ width: '100%', borderRadius: '8px 8px 0 0' }}
/>
) : (
<img
src={asset.url}
alt={asset.prompt}
style={{ width: '100%', borderRadius: '8px 8px 0 0' }}
/>
)}
<VStack p={3} align="stretch" gap={2}>
<HStack justify="space-between">
<Badge colorPalette={asset.type === 'video' ? 'purple' : 'blue'}>
{asset.type === 'video' ? <LuVideo /> : <LuImage />}
{asset.type.charAt(0).toUpperCase() + asset.type.slice(1)}
</Badge>
<Text fontSize="xs" color="fg.muted">
{new Date(asset.createdAt).toLocaleString('tr-TR')}
</Text>
</HStack>
<Text fontSize="sm" lineClamp={2}>{asset.prompt}</Text>
<HStack justify="space-between">
<Button
size="xs"
variant="ghost"
onClick={() => handleCopyPrompt(asset.prompt)}
>
<LuCopy /> Prompt
</Button>
<Button
size="xs"
colorPalette="green"
onClick={() => handleDownload(asset.url, `${asset.type}-${asset.id}.${asset.type === 'video' ? 'mp4' : 'png'}`)}
>
<LuDownload /> İndir
</Button>
</HStack>
</VStack>
</Card.Body>
</Card.Root>
))}
</SimpleGrid>
</Box>
)}
{/* Empty State */}
{generatedAssets.length === 0 && (
<Card.Root>
<Card.Body>
<VStack py={10} gap={4}>
<Icon as={LuImage} boxSize={12} color="fg.muted" />
<Text color="fg.muted" textAlign="center">
Henüz görsel oluşturulmadı.<br />
Bir konu girerek başlayın.
</Text>
</VStack>
</Card.Body>
</Card.Root>
)}
</VStack>
);
}

View File

@@ -1,14 +0,0 @@
import { getTranslations } from "next-intl/server";
import HomeCard from "@/components/site/home/home-card";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("home")} | FCS`,
};
}
export default function Home() {
return <HomeCard />;
}

View File

@@ -1,7 +1,7 @@
'use client';
import { Container, Flex } from '@chakra-ui/react';
import Header from '@/components/layout/header/header';
import { Header } from '@/components/layout/header/header';
import Footer from '@/components/layout/footer/footer';
import BackToTop from '@/components/ui/back-to-top';

View File

@@ -0,0 +1,97 @@
// API Proxy Route - Forwards requests to backend
// Path: src/app/api/backend/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server';
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3001';
async function proxyRequest(request: NextRequest, pathSegments: string[]) {
const targetPath = pathSegments.join('/');
const targetUrl = `${BACKEND_URL}/api/${targetPath}`;
console.log(`[Proxy] Forwarding ${request.method} to ${targetUrl}`);
// Get request body for non-GET requests
let body: string | undefined;
if (request.method !== 'GET' && request.method !== 'HEAD') {
try {
body = await request.text();
} catch {
// No body
}
}
// Forward headers (excluding host)
const headers: HeadersInit = {};
request.headers.forEach((value, key) => {
if (key.toLowerCase() !== 'host') {
headers[key] = value;
}
});
try {
const response = await fetch(targetUrl, {
method: request.method,
headers,
body: body || undefined,
});
// Get response data
const contentType = response.headers.get('content-type') || '';
let data: string | ArrayBuffer;
if (contentType.includes('application/json')) {
data = await response.text();
} else {
data = await response.arrayBuffer();
}
// Build response headers
const responseHeaders = new Headers();
response.headers.forEach((value, key) => {
// Skip certain headers
if (!['content-encoding', 'transfer-encoding'].includes(key.toLowerCase())) {
responseHeaders.set(key, value);
}
});
return new NextResponse(data, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (error) {
console.error('Proxy error:', error);
return NextResponse.json(
{
success: false,
error: 'Backend unavailable',
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 502 }
);
}
}
// Next.js 16 uses context.params directly (not a Promise)
type RouteContext = { params: { path: string[] } };
export async function GET(request: NextRequest, context: RouteContext) {
return proxyRequest(request, context.params.path);
}
export async function POST(request: NextRequest, context: RouteContext) {
return proxyRequest(request, context.params.path);
}
export async function PUT(request: NextRequest, context: RouteContext) {
return proxyRequest(request, context.params.path);
}
export async function PATCH(request: NextRequest, context: RouteContext) {
return proxyRequest(request, context.params.path);
}
export async function DELETE(request: NextRequest, context: RouteContext) {
return proxyRequest(request, context.params.path);
}

View File

@@ -0,0 +1,83 @@
"use client";
import { Box, Text, VStack, HStack, Badge, Separator } from "@chakra-ui/react";
import {
DialogBody,
DialogCloseTrigger,
DialogContent,
DialogHeader,
DialogRoot,
DialogTitle,
} from "@/components/ui/overlays/dialog";
import { LuCalendar, LuGlobe, LuFileText } from "react-icons/lu";
interface ContentItem {
id: number;
title: string;
platform: string;
status: string;
date: string;
}
interface ContentPreviewDialogProps {
item: ContentItem | null;
open: boolean;
onOpenChange: (details: { open: boolean }) => void;
}
const getStatusColor = (status: string) => {
switch (status) {
case "published": return "green";
case "draft": return "gray";
case "scheduled": return "blue";
case "review": return "orange";
default: return "gray";
}
};
export function ContentPreviewDialog({ item, open, onOpenChange }: ContentPreviewDialogProps) {
if (!item) return null;
return (
<DialogRoot open={open} onOpenChange={onOpenChange} size="lg">
<DialogContent>
<DialogHeader>
<DialogTitle>{item.title}</DialogTitle>
</DialogHeader>
<DialogBody pb={8}>
<VStack gap={4} align="stretch">
<HStack gap={4}>
<Badge colorPalette={getStatusColor(item.status)} size="lg" variant="solid">
{item.status.toUpperCase()}
</Badge>
<HStack color="fg.muted">
<LuGlobe />
<Text>{item.platform}</Text>
</HStack>
<HStack color="fg.muted">
<LuCalendar />
<Text>{item.date}</Text>
</HStack>
</HStack>
<Separator />
<Box p={4} bg="bg.subtle" borderRadius="md">
<HStack mb={2} color="fg.muted">
<LuFileText />
<Text fontWeight="medium">Content Preview</Text>
</HStack>
<Text>
This is a preview of the content for "{item.title}". In a real scenario, this would show the full post text, images, or video script generated by the system.
</Text>
<Text mt={4} fontStyle="italic" color="fg.muted">
Generated content will appear here...
</Text>
</Box>
</VStack>
</DialogBody>
<DialogCloseTrigger />
</DialogContent>
</DialogRoot>
);
}

View File

@@ -0,0 +1,111 @@
"use client";
import { Box, Table, Badge, HStack, IconButton } from "@chakra-ui/react";
import { LuEye, LuPencil, LuTrash2 } from "react-icons/lu";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
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) => {
switch (status) {
case "published": return "green";
case "draft": return "gray";
case "scheduled": return "blue";
case "review": return "orange";
default: return "gray";
}
};
export function ContentTable() {
const [selectedItem, setSelectedItem] = useState<any>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const handleAction = (action: string, item: any) => {
if (action === 'View') {
setSelectedItem(item);
setIsPreviewOpen(true);
return;
}
toaster.create({
title: `${action} Action`,
description: `You clicked ${action} for ${item.title}`,
type: "info",
});
};
return (
<>
<Box borderWidth="1px" borderRadius="lg" overflow="hidden" bg="bg.panel">
<Table.Root striped>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>Title</Table.ColumnHeader>
<Table.ColumnHeader>Platform</Table.ColumnHeader>
<Table.ColumnHeader>Status</Table.ColumnHeader>
<Table.ColumnHeader>Date</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">Actions</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{MOCK_CONTENT.map((item) => (
<Table.Row key={item.id}>
<Table.Cell fontWeight="medium">{item.title}</Table.Cell>
<Table.Cell>{item.platform}</Table.Cell>
<Table.Cell>
<Badge colorPalette={getStatusColor(item.status)} variant="solid">
{item.status}
</Badge>
</Table.Cell>
<Table.Cell>{item.date}</Table.Cell>
<Table.Cell textAlign="right">
<HStack justify="flex-end" gap={2}>
<IconButton
variant="ghost"
size="sm"
aria-label="View"
onClick={() => handleAction('View', item)}
>
<LuEye />
</IconButton>
<IconButton
variant="ghost"
size="sm"
aria-label="Edit"
onClick={() => handleAction('Edit', item)}
>
<LuPencil />
</IconButton>
<IconButton
variant="ghost"
size="sm"
colorPalette="red"
aria-label="Delete"
onClick={() => handleAction('Delete', item)}
>
<LuTrash2 />
</IconButton>
</HStack>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Box>
<ContentPreviewDialog
item={selectedItem}
open={isPreviewOpen}
onOpenChange={(e) => setIsPreviewOpen(e.open)}
/>
</>
);
}

View File

@@ -0,0 +1,340 @@
"use client";
import { Box, Heading, Steps, VStack, Input, Button, Text, HStack, Textarea, Card, SimpleGrid, Badge } from "@chakra-ui/react";
import { LuSparkles, LuArrowRight, LuCheck, LuHash } from "react-icons/lu";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState, useEffect } from "react";
import { GeneratedContentResult } from "./GeneratedContentResult";
import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
// Platform Data (can also be fetched from backend if dynamic)
const PLATFORMS = [
{ id: 'twitter', name: 'Twitter/X', icon: '𝕏' },
{ id: 'linkedin', name: 'LinkedIn', icon: '💼' },
{ id: 'instagram', name: 'Instagram', icon: '📸' },
{ id: 'medium', name: 'Medium/Blog', icon: '📝' },
];
const STEPS = [
{ title: "Topic & Niche", description: "Define what to create" },
{ title: "Platforms", description: "Where to post" },
{ title: "Review", description: "Generate content" },
];
interface Niche {
id: string;
name: string;
description?: string;
}
export function GenerateWizard() {
const { data: session } = useSession();
const searchParams = useSearchParams();
const [activeStep, setActiveStep] = useState(0);
const [topic, setTopic] = useState("");
const [trendDescription, setTrendDescription] = useState("");
const [trendKeywords, setTrendKeywords] = useState<string[]>([]);
const [trendSource, setTrendSource] = useState("");
const [selectedNiche, setSelectedNiche] = useState("");
const [selectedPlatforms, setSelectedPlatforms] = useState<string[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [niches, setNiches] = useState<Niche[]>([]);
const [isLoadingNiches, setIsLoadingNiches] = useState(false);
const [generatedBundle, setGeneratedBundle] = useState<any>(null);
// Read trend data from URL params (from trends page)
useEffect(() => {
const topicParam = searchParams.get('topic');
const descParam = searchParams.get('description');
const keywordsParam = searchParams.get('keywords');
const sourceParam = searchParams.get('source');
if (topicParam) setTopic(decodeURIComponent(topicParam));
if (descParam) setTrendDescription(decodeURIComponent(descParam));
if (sourceParam) setTrendSource(sourceParam);
if (keywordsParam) {
try {
const parsed = JSON.parse(keywordsParam);
if (Array.isArray(parsed)) setTrendKeywords(parsed);
} catch (e) {
console.error('Failed to parse keywords:', e);
}
}
}, [searchParams]);
// Fetch niches on mount
useEffect(() => {
async function fetchNiches() {
setIsLoadingNiches(true);
try {
const headers: HeadersInit = {};
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
// Using a relative path which the proxy should handle
const res = await fetch('/api/backend/content-generation/niches', { headers });
if (res.ok) {
const data = await res.json();
if (Array.isArray(data)) {
setNiches(data);
} else {
console.error("API returned non-array for niches:", data);
setNiches([]);
}
} else {
console.error("Failed to fetch niches", res.status);
setNiches([]);
}
} catch (error) {
console.error("Error fetching niches:", error);
} finally {
setIsLoadingNiches(false);
}
}
if (session) {
fetchNiches();
}
}, [session]);
const togglePlatform = (id: string) => {
setSelectedPlatforms(prev =>
prev.includes(id) ? prev.filter(p => p !== id) : [...prev, id]
);
};
const handleNext = () => {
if (activeStep === 0 && !topic) {
toaster.create({
title: "Topic Required",
description: "Please enter a topic to proceed.",
type: "error"
});
return;
}
if (activeStep === 1 && selectedPlatforms.length === 0) {
toaster.create({
title: "Platform Required",
description: "Select at least one platform.",
type: "error"
});
return;
}
setActiveStep((prev) => Math.min(prev + 1, STEPS.length - 1));
};
const handleBack = () => {
setActiveStep((prev) => Math.max(prev - 1, 0));
};
const handleGenerate = async () => {
setIsGenerating(true);
try {
const payload = {
topic,
description: trendDescription || undefined,
keywords: trendKeywords.length > 0 ? trendKeywords : undefined,
niche: selectedNiche, // The service expects 'niche' as string ID
platforms: selectedPlatforms,
includeResearch: true,
includeHashtags: true,
brandVoice: "friendly-expert", // Default for now, can be added to UI
count: 1
};
const headers: HeadersInit = { 'Content-Type': 'application/json' };
if (session?.accessToken) {
headers['Authorization'] = `Bearer ${session.accessToken}`;
}
const response = await fetch('/api/backend/content-generation/generate', {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error("Generation failed");
const data = await response.json();
setGeneratedBundle(data);
toaster.create({
title: "Content Generated",
description: "Your content is ready!",
type: "success"
});
} catch (error) {
console.error(error);
toaster.create({
title: "Error",
description: "Failed to generate content. Please try again.",
type: "error"
});
} finally {
setIsGenerating(false);
}
};
if (generatedBundle) {
return <GeneratedContentResult bundle={generatedBundle} onReset={() => {
setGeneratedBundle(null);
setActiveStep(0);
setTopic("");
setSelectedPlatforms([]);
}} />;
}
return (
<Box>
<Box mb={8}>
<Steps.Root index={activeStep} count={STEPS.length}>
<Steps.List>
{STEPS.map((step, index) => (
<Steps.Item key={index} title={step.title} icon={<Box>{index + 1}</Box>}>
{step.description}
</Steps.Item>
))}
</Steps.List>
</Steps.Root>
</Box>
<Box p={6} borderWidth="1px" borderRadius="lg" bg="bg.panel">
{activeStep === 0 && (
<VStack align="stretch" gap={6}>
<Heading size="md">1. Start with a Topic</Heading>
{/* Show trend info if coming from trends page */}
{(trendSource || trendDescription || trendKeywords.length > 0) && (
<Card.Root bg="blue.50/10" borderColor="blue.500" borderWidth="1px">
<Card.Body>
<VStack align="start" gap={2}>
<HStack>
<Badge colorPalette="blue">📊 Trend'den Geldi</Badge>
{trendSource && <Badge variant="outline">{trendSource}</Badge>}
</HStack>
{trendDescription && (
<Text fontSize="sm" color="fg.muted">{trendDescription}</Text>
)}
{trendKeywords.length > 0 && (
<HStack flexWrap="wrap" gap={1}>
{trendKeywords.map((kw, i) => (
<Badge key={i} size="sm" variant="subtle">
<LuHash /> {kw}
</Badge>
))}
</HStack>
)}
</VStack>
</Card.Body>
</Card.Root>
)}
<Box>
<Text mb={2} fontWeight="medium">What do you want to post about?</Text>
<Input
placeholder="e.g. 5 ways to use AI for marketing..."
value={topic}
onChange={(e) => setTopic(e.target.value)}
size="lg"
/>
</Box>
<Box>
<Text mb={3} fontWeight="medium">Select a Niche (Optional)</Text>
{/* Fallback mock niches if fetch fails/empty for demo purposes */}
<SimpleGrid columns={{ base: 2, md: 3 }} gap={4}>
{(niches.length > 0 ? niches : [
{ id: 'personal-finance', name: 'Personal Finance' },
{ id: 'productivity', name: 'Productivity' },
{ id: 'ai-tech', name: 'AI & Tech' },
{ id: 'marketing', name: 'Marketing' }
]).map((niche) => (
<Card.Root
key={niche.id}
cursor="pointer"
onClick={() => setSelectedNiche(niche.id)}
borderColor={selectedNiche === niche.id ? "blue.500" : "border"}
borderWidth={selectedNiche === niche.id ? "2px" : "1px"}
bg={selectedNiche === niche.id ? "blue.50/10" : "bg.subtle"}
_hover={{ borderColor: "blue.500" }}
>
<Card.Body p={4}>
<HStack justify="space-between">
<Text fontWeight="medium" fontSize="sm">{niche.name}</Text>
{selectedNiche === niche.id && <LuCheck color="var(--chakra-colors-blue-500)" />}
</HStack>
</Card.Body>
</Card.Root>
))}
</SimpleGrid>
</Box>
</VStack>
)}
{activeStep === 1 && (
<VStack align="stretch" gap={6}>
<Heading size="md">2. Select Platforms</Heading>
<Text mb={2} color="fg.muted">Where should this content be posted?</Text>
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4}>
{PLATFORMS.map((platform) => (
<Card.Root
key={platform.id}
cursor="pointer"
onClick={() => togglePlatform(platform.id)}
borderColor={selectedPlatforms.includes(platform.id) ? "blue.500" : "border"}
borderWidth={selectedPlatforms.includes(platform.id) ? "2px" : "1px"}
bg={selectedPlatforms.includes(platform.id) ? "blue.50/10" : "bg.subtle"}
_hover={{ borderColor: "blue.500" }}
>
<Card.Body p={4} textAlign="center">
<VStack>
<Text fontSize="2xl">{platform.icon}</Text>
<Text fontWeight="medium">{platform.name}</Text>
{selectedPlatforms.includes(platform.id) && <LuCheck color="var(--chakra-colors-blue-500)" />}
</VStack>
</Card.Body>
</Card.Root>
))}
</SimpleGrid>
</VStack>
)}
{activeStep === 2 && (
<VStack align="stretch" gap={4}>
<Heading size="md">3. Ready to Generate?</Heading>
<Card.Root>
<Card.Body>
<VStack align="start" gap={2}>
<Text><strong>Topic:</strong> {topic}</Text>
<Text><strong>Niche:</strong> {niches?.find((n) => n.id === selectedNiche)?.name || selectedNiche || "General"}</Text>
<Text><strong>Platforms:</strong> {selectedPlatforms.map(p => PLATFORMS.find(pl => pl.id === p)?.name).join(", ")}</Text>
</VStack>
</Card.Body>
</Card.Root>
</VStack>
)}
<HStack justify="flex-end" mt={8} gap={4}>
<Button variant="outline" onClick={handleBack} disabled={activeStep === 0 || isGenerating}>
Back
</Button>
{activeStep === STEPS.length - 1 ? (
<Button onClick={handleGenerate} loading={isGenerating} loadingText="Generating...">
Generate <LuSparkles />
</Button>
) : (
<Button onClick={handleNext}>
Next <LuArrowRight />
</Button>
)}
</HStack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,429 @@
"use client";
import { Box, Heading, VStack, Text, Card, HStack, Badge, Button, Tabs, SimpleGrid, Progress, Icon } from "@chakra-ui/react";
import { LuCopy, LuCheck, LuFileText, LuBrainCircuit, LuLayers, LuSparkles, LuSearch, LuZap, LuDownload, LuHash } from "react-icons/lu";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
// Matches backend GeneratedContentBundle
interface GeneratedContentBundle {
id: string;
topic: string;
niche?: any;
research?: {
summary: string;
keyFindings: { finding: string; confidence: string }[];
statistics: { value: string; context: string }[];
quotes: { text: string; author: string }[];
};
platforms: {
platform: string;
content: string;
hashtags: string[];
characterCount: number;
postingRecommendation: string;
// mediaRecommendations is string[] in backend
mediaRecommendations: string[];
}[];
variations?: {
original: string;
variations: { type: string; content: string }[];
}[];
seo?: {
score: number;
keywords: string[];
suggestions: string[];
meta: { title: string; description: string };
};
neuro?: {
score: number;
triggersUsed: string[];
emotionProfile: string[];
improvements: string[];
};
createdAt: string;
}
interface Props {
bundle: GeneratedContentBundle;
onReset: () => void;
}
export function GeneratedContentResult({ bundle, onReset }: Props) {
const [copiedId, setCopiedId] = useState<string | null>(null);
const handleCopy = (text: string, id: string) => {
navigator.clipboard.writeText(text);
setCopiedId(id);
toaster.create({
title: "Copied to clipboard",
type: "success",
duration: 2000,
});
setTimeout(() => setCopiedId(null), 2000);
};
const handleCopyWithHashtags = (content: string, hashtags: string[], id: string) => {
const fullText = `${content}\n\n${hashtags.join(' ')}`;
navigator.clipboard.writeText(fullText);
setCopiedId(id + '-full');
toaster.create({ title: "Copied with hashtags", type: "success", duration: 2000 });
setTimeout(() => setCopiedId(null), 2000);
};
const handleDownloadText = (content: string, platform: string) => {
const blob = new Blob([content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${bundle.topic}-${platform}.txt`;
a.click();
window.URL.revokeObjectURL(url);
};
if (!bundle || !bundle.platforms || bundle.platforms.length === 0) {
return (
<VStack gap={4} py={10}>
<Text color="fg.muted">No content generated. Please try again.</Text>
<Button variant="outline" onClick={onReset}>Try Again</Button>
</VStack>
);
}
return (
<VStack align="stretch" gap={6}>
<HStack justify="space-between">
<Heading size="lg">Generated Results</Heading>
<Button variant="outline" onClick={onReset}>Create New</Button>
</HStack>
<Tabs.Root defaultValue="content">
<Tabs.List>
<Tabs.Trigger value="content">
<LuFileText /> Content
</Tabs.Trigger>
{bundle.research && (
<Tabs.Trigger value="research">
<LuBrainCircuit /> Deep Research
</Tabs.Trigger>
)}
{bundle.variations && bundle.variations.length > 0 && (
<Tabs.Trigger value="variations">
<LuLayers /> Variations
</Tabs.Trigger>
)}
{bundle.seo && (
<Tabs.Trigger value="seo">
<LuSearch /> SEO
</Tabs.Trigger>
)}
{bundle.neuro && (
<Tabs.Trigger value="neuro">
<LuZap /> Neuro
</Tabs.Trigger>
)}
</Tabs.List>
{/* CONTENT TAB */}
<Tabs.Content value="content">
<Tabs.Root defaultValue={bundle.platforms[0]?.platform}>
<Tabs.List>
{bundle.platforms.map(p => (
<Tabs.Trigger key={p.platform} value={p.platform}>
{p.platform.charAt(0).toUpperCase() + p.platform.slice(1)}
</Tabs.Trigger>
))}
</Tabs.List>
{bundle.platforms.map((item) => (
<Tabs.Content key={item.platform} value={item.platform}>
<Card.Root>
<Card.Body>
<VStack align="stretch" gap={4}>
<HStack justify="space-between">
<Badge colorPalette="blue" variant="subtle">
{item.characterCount} chars
</Badge>
<Button
size="sm"
variant="ghost"
onClick={() => handleCopy(item.content, item.platform)}
>
{copiedId === item.platform ? <LuCheck /> : <LuCopy />}
{copiedId === item.platform ? "Copied" : "Copy"}
</Button>
</HStack>
<Box
p={4}
bg="bg.subtle"
borderRadius="md"
whiteSpace="pre-wrap"
fontFamily="mono"
fontSize="sm"
>
{item.content}
</Box>
<Box mt={2}>
<Text fontWeight="medium" mb={1} fontSize="sm">Hashtags:</Text>
<Text color="blue.500" fontSize="sm">{item.hashtags.join(" ")}</Text>
</Box>
{item.postingRecommendation && (
<Box mt={2} p={2} bg="blue.50/10" borderRadius="md">
<HStack>
<LuSparkles color="var(--chakra-colors-blue-500)" />
<Text fontSize="sm" color="fg.muted">{item.postingRecommendation}</Text>
</HStack>
</Box>
)}
<HStack mt={3} gap={2}>
<Button
size="sm"
variant="outline"
onClick={() => handleCopyWithHashtags(item.content, item.hashtags, item.platform)}
>
<LuHash /> Copy + Hashtags
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDownloadText(item.content, item.platform)}
>
<LuDownload /> Download
</Button>
</HStack>
</VStack>
</Card.Body>
</Card.Root>
</Tabs.Content>
))}
</Tabs.Root>
</Tabs.Content>
{/* RESEARCH TAB */}
<Tabs.Content value="research">
<VStack align="stretch" gap={6}>
<Card.Root>
<Card.Header>
<Heading size="md">Executive Summary</Heading>
</Card.Header>
<Card.Body>
<Text>{bundle.research?.summary}</Text>
</Card.Body>
</Card.Root>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
<Card.Root>
<Card.Header>
<Heading size="sm">Key Findings</Heading>
</Card.Header>
<Card.Body>
<VStack align="start" gap={3}>
{bundle.research?.keyFindings.map((finding, idx) => (
<HStack key={idx} align="start">
<LuCheck color="green" style={{ marginTop: '4px' }} />
<Text fontSize="sm">{finding.finding}</Text>
</HStack>
))}
</VStack>
</Card.Body>
</Card.Root>
<Card.Root>
<Card.Header>
<Heading size="sm">Statistics</Heading>
</Card.Header>
<Card.Body>
<VStack align="start" gap={3}>
{bundle.research?.statistics.map((stat, idx) => (
<Box key={idx}>
<Text fontWeight="bold" color="blue.500">{stat.value}</Text>
<Text fontSize="sm" color="fg.muted">{stat.context}</Text>
</Box>
))}
</VStack>
</Card.Body>
</Card.Root>
</SimpleGrid>
</VStack>
</Tabs.Content>
{/* VARIATIONS TAB */}
<Tabs.Content value="variations">
<VStack align="stretch" gap={4}>
{bundle.variations?.map((varSet, i) => (
<Box key={i}>
{varSet.variations.map((variation, j) => (
<Card.Root key={j} mb={4}>
<Card.Header>
<Badge>{variation.type}</Badge>
</Card.Header>
<Card.Body>
<Box
p={3}
bg="bg.subtle"
borderRadius="md"
whiteSpace="pre-wrap"
fontFamily="mono"
fontSize="sm"
>
{variation.content}
</Box>
<Button
size="sm"
variant="ghost"
mt={2}
onClick={() => handleCopy(variation.content, `var-${i}-${j}`)}
>
<LuCopy /> Copy
</Button>
</Card.Body>
</Card.Root>
))}
</Box>
))}
</VStack>
</Tabs.Content>
{/* SEO TAB */}
<Tabs.Content value="seo">
<VStack align="stretch" gap={4}>
<Card.Root>
<Card.Header>
<HStack justify="space-between">
<Heading size="md">SEO Score</Heading>
<Badge
colorPalette={bundle.seo?.score && bundle.seo.score >= 70 ? 'green' : bundle.seo?.score && bundle.seo.score >= 50 ? 'yellow' : 'red'}
size="lg"
>
{bundle.seo?.score || 0}/100
</Badge>
</HStack>
</Card.Header>
<Card.Body>
<Progress.Root value={bundle.seo?.score || 0} max={100} size="lg">
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
</Card.Body>
</Card.Root>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
<Card.Root>
<Card.Header>
<Heading size="sm">Keywords</Heading>
</Card.Header>
<Card.Body>
<HStack flexWrap="wrap" gap={2}>
{bundle.seo?.keywords?.map((kw, idx) => (
<Badge key={idx} variant="subtle" colorPalette="blue">
{kw}
</Badge>
))}
</HStack>
</Card.Body>
</Card.Root>
<Card.Root>
<Card.Header>
<Heading size="sm">Suggestions</Heading>
</Card.Header>
<Card.Body>
<VStack align="start" gap={2}>
{bundle.seo?.suggestions?.map((sug, idx) => (
<HStack key={idx}>
<LuCheck color="green" />
<Text fontSize="sm">{sug}</Text>
</HStack>
))}
</VStack>
</Card.Body>
</Card.Root>
</SimpleGrid>
</VStack>
</Tabs.Content>
{/* NEURO TAB */}
<Tabs.Content value="neuro">
<VStack align="stretch" gap={4}>
<Card.Root>
<Card.Header>
<HStack justify="space-between">
<Heading size="md">Neuro Score</Heading>
<Badge
colorPalette={bundle.neuro?.score && bundle.neuro.score >= 70 ? 'green' : bundle.neuro?.score && bundle.neuro.score >= 50 ? 'yellow' : 'red'}
size="lg"
>
{bundle.neuro?.score || 0}/100
</Badge>
</HStack>
</Card.Header>
<Card.Body>
<Progress.Root value={bundle.neuro?.score || 0} max={100} size="lg">
<Progress.Track>
<Progress.Range />
</Progress.Track>
</Progress.Root>
</Card.Body>
</Card.Root>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
<Card.Root>
<Card.Header>
<Heading size="sm">Triggers Used</Heading>
</Card.Header>
<Card.Body>
<HStack flexWrap="wrap" gap={2}>
{bundle.neuro?.triggersUsed?.map((trigger, idx) => (
<Badge key={idx} variant="subtle" colorPalette="purple">
{trigger}
</Badge>
))}
{(!bundle.neuro?.triggersUsed || bundle.neuro.triggersUsed.length === 0) && (
<Text fontSize="sm" color="fg.muted">No triggers detected</Text>
)}
</HStack>
</Card.Body>
</Card.Root>
<Card.Root>
<Card.Header>
<Heading size="sm">Improvements</Heading>
</Card.Header>
<Card.Body>
<VStack align="start" gap={2}>
{bundle.neuro?.improvements?.map((imp, idx) => (
<HStack key={idx}>
<LuZap color="orange" />
<Text fontSize="sm">{imp}</Text>
</HStack>
))}
</VStack>
</Card.Body>
</Card.Root>
</SimpleGrid>
<Card.Root>
<Card.Header>
<Heading size="sm">Emotion Profile</Heading>
</Card.Header>
<Card.Body>
<HStack flexWrap="wrap" gap={2}>
{bundle.neuro?.emotionProfile?.map((emotion, idx) => (
<Badge key={idx} variant="outline" colorPalette="green">
{emotion}
</Badge>
))}
</HStack>
</Card.Body>
</Card.Root>
</VStack>
</Tabs.Content>
</Tabs.Root>
</VStack>
);
}

View File

@@ -0,0 +1,24 @@
'use client';
import { Box, Flex } from '@chakra-ui/react';
import { Sidebar } from '../sidebar/Sidebar';
import { Header } from '../header/header';
import { ReactNode } from 'react';
interface DashboardLayoutProps {
children: ReactNode;
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
return (
<Flex minH="100vh" bg="bg.canvas">
<Sidebar />
<Box flex="1" ml="250px" transition="margin-left 0.2s">
<Header />
<Box as="main" p={8}>
{children}
</Box>
</Box>
</Flex>
);
}

View File

@@ -1,229 +1,50 @@
"use client";
'use client';
import {
Box,
Flex,
HStack,
IconButton,
Link as ChakraLink,
Stack,
VStack,
Button,
MenuItem,
ClientOnly,
} from "@chakra-ui/react";
import { Link, useRouter } from "@/i18n/navigation";
import { ColorModeButton } from "@/components/ui/color-mode";
import {
PopoverBody,
PopoverContent,
PopoverRoot,
PopoverTrigger,
} from "@/components/ui/overlays/popover";
import { RxHamburgerMenu } from "react-icons/rx";
import { NAV_ITEMS } from "@/config/navigation";
import HeaderLink from "./header-link";
import MobileHeaderLink from "./mobile-header-link";
import LocaleSwitcher from "@/components/ui/locale-switcher";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import {
MenuContent,
MenuRoot,
MenuTrigger,
} from "@/components/ui/overlays/menu";
import { Avatar } from "@/components/ui/data-display/avatar";
import { Skeleton } from "@/components/ui/feedback/skeleton";
import { signOut, useSession } from "next-auth/react";
import { authConfig } from "@/config/auth";
import { LoginModal } from "@/components/auth/login-modal";
import { LuLogIn } from "react-icons/lu";
import { Flex, Text, HStack, Box } from '@chakra-ui/react';
import { ColorModeButton } from '@/components/ui/color-mode';
import { Avatar } from '@/components/ui/data-display/avatar';
import { usePathname } from 'next/navigation';
export default function Header() {
const t = useTranslations();
const [isSticky, setIsSticky] = useState(false);
const [loginModalOpen, setLoginModalOpen] = useState(false);
const router = useRouter();
const { data: session, status } = useSession();
export function Header() {
const pathname = usePathname();
const isAuthenticated = !!session;
const isLoading = status === "loading";
useEffect(() => {
const handleScroll = () => {
setIsSticky(window.scrollY >= 10);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
const handleLogout = async () => {
await signOut({ redirect: false });
if (authConfig.isAuthRequired) {
router.replace("/signin");
}
};
// Render user menu or login button based on auth state
const renderAuthSection = () => {
if (isLoading) {
return <Skeleton boxSize="10" rounded="full" />;
}
if (isAuthenticated) {
return (
<MenuRoot positioning={{ placement: "bottom-start" }}>
<MenuTrigger rounded="full" focusRing="none">
<Avatar name={session?.user?.name || "User"} variant="solid" />
</MenuTrigger>
<MenuContent>
<MenuItem onClick={handleLogout} value="sign-out">
{t("auth.sign-out")}
</MenuItem>
</MenuContent>
</MenuRoot>
);
}
// Not authenticated - show login button
return (
<Button
variant="solid"
colorPalette="primary"
size="sm"
onClick={() => setLoginModalOpen(true)}
>
<LuLogIn />
{t("auth.sign-in")}
</Button>
);
};
// Render mobile auth section
const renderMobileAuthSection = () => {
if (isLoading) {
return <Skeleton height="10" width="full" />;
}
if (isAuthenticated) {
return (
<>
<Avatar name={session?.user?.name || "User"} variant="solid" />
<Button
variant="surface"
size="sm"
width="full"
onClick={handleLogout}
>
{t("auth.sign-out")}
</Button>
</>
);
}
return (
<Button
variant="solid"
colorPalette="primary"
size="sm"
width="full"
onClick={() => setLoginModalOpen(true)}
>
<LuLogIn />
{t("auth.sign-in")}
</Button>
);
const getPageTitle = (path: string) => {
if (path === '/home') return 'Dashboard';
if (path.startsWith('/content')) return 'Content Management';
if (path.startsWith('/schedule')) return 'Schedule & Calendar';
if (path.startsWith('/analytics')) return 'Analytics & Insights';
if (path.startsWith('/settings')) return 'Settings';
return 'Dashboard';
};
return (
<>
<Box
as="nav"
bg={isSticky ? "rgba(255, 255, 255, 0.6)" : "white"}
_dark={{
bg: isSticky ? "rgba(1, 1, 1, 0.6)" : "black",
}}
shadow={isSticky ? "sm" : "none"}
backdropFilter="blur(12px) saturate(180%)"
border="1px solid"
borderColor={isSticky ? "whiteAlpha.300" : "transparent"}
borderBottomRadius={isSticky ? "xl" : "none"}
transition="all 0.4s ease-in-out"
px={{ base: 4, md: 8 }}
py="3"
<Flex
as="header"
h="64px"
bg="bg.panel"
borderBottomWidth="1px"
borderColor="border.subtle"
align="center"
justify="space-between"
px={8}
position="sticky"
top={0}
zIndex={10}
w="full"
zIndex={99}
>
<Flex justify="space-between" align="center" maxW="8xl" mx="auto">
{/* Logo */}
<HStack>
<ChakraLink
as={Link}
href="/home"
fontSize="lg"
fontWeight="bold"
color={{ base: "primary.500", _dark: "primary.300" }}
focusRing="none"
textDecor="none"
transition="all 0.3s ease-in-out"
_hover={{
color: { base: "primary.900", _dark: "primary.50" },
}}
>
{"FCS "}
</ChakraLink>
</HStack>
<Text fontSize="lg" fontWeight="semibold" color="fg.default">
{getPageTitle(pathname || '')}
</Text>
{/* DESKTOP NAVIGATION */}
<HStack spaceX={4} display={{ base: "none", lg: "flex" }}>
{NAV_ITEMS.map((item, index) => (
<HeaderLink key={index} item={item} />
))}
</HStack>
<HStack>
<ColorModeButton colorPalette="gray" />
<Box display={{ base: "none", lg: "inline-flex" }} gap={2}>
<LocaleSwitcher />
<ClientOnly fallback={<Skeleton boxSize="10" rounded="full" />}>
{renderAuthSection()}
</ClientOnly>
<HStack gap={4}>
<ColorModeButton />
<HStack gap={3}>
<Box textAlign="right" display={{ base: 'none', md: 'block' }}>
<Text textStyle="sm" fontWeight="medium">John Doe</Text>
<Text textStyle="xs" color="fg.muted">Admin</Text>
</Box>
{/* MOBILE NAVIGATION */}
<Stack display={{ base: "inline-flex", lg: "none" }}>
<ClientOnly fallback={<Skeleton boxSize="9" />}>
<PopoverRoot>
<PopoverTrigger as="span">
<IconButton aria-label="Open menu" variant="ghost">
<RxHamburgerMenu />
</IconButton>
</PopoverTrigger>
<PopoverContent width={{ base: "xs", sm: "sm", md: "md" }}>
<PopoverBody>
<VStack mt="2" align="start" spaceY="2" w="full">
{NAV_ITEMS.map((item) => (
<MobileHeaderLink key={item.label} item={item} />
))}
<LocaleSwitcher />
{renderMobileAuthSection()}
</VStack>
</PopoverBody>
</PopoverContent>
</PopoverRoot>
</ClientOnly>
</Stack>
<Avatar name="John Doe" src="https://bit.ly/dan-abramov" size="sm" />
</HStack>
</HStack>
</Flex>
</Box>
{/* Login Modal */}
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
</>
);
}

View File

@@ -0,0 +1,102 @@
'use client';
import { Box, Flex, VStack, Text, Icon, Link as ChakraLink, Separator } from '@chakra-ui/react';
import { Link, usePathname } from '@/i18n/navigation';
import { LuLayoutDashboard, LuFileText, LuCalendar, LuTrendingUp, LuSettings, LuLogOut, LuSparkles } from 'react-icons/lu';
import { NAV_ITEMS as CONFIG_NAV_ITEMS } from '@/config/navigation';
const ICON_MAP: Record<string, any> = {
'/home': LuLayoutDashboard,
'/generate': LuSparkles,
'/content': LuFileText,
'/schedule': LuCalendar,
'/analytics': LuTrendingUp,
'/settings': LuSettings,
};
const NAV_ITEMS = CONFIG_NAV_ITEMS.map(item => ({
name: item.label.charAt(0).toUpperCase() + item.label.slice(1),
href: item.href,
icon: ICON_MAP[item.href] || LuFileText
}));
export function Sidebar() {
const pathname = usePathname();
return (
<Box
w="250px"
h="100vh"
bg="bg.panel"
borderRightWidth="1px"
borderColor="border.subtle"
position="fixed"
left={0}
top={0}
zIndex={100}
py={6}
px={4}
>
<Flex align="center" gap={3} px={2} mb={8}>
<Box w={8} h={8} bg="primary.solid" borderRadius="lg" display="flex" alignItems="center" justifyContent="center">
<Text color="white" fontWeight="bold">CH</Text>
</Box>
<Text fontSize="xl" fontWeight="bold">Content Hunter</Text>
</Flex>
<VStack gap={2} align="stretch">
{NAV_ITEMS.map((item) => {
const isActive = pathname === item.href || pathname?.startsWith(item.href + '/');
return (
<ChakraLink
as={Link}
key={item.name}
href={item.href}
display="flex"
alignItems="center"
gap={3}
px={3}
py={2.5}
borderRadius="md"
textDecoration="none"
bg={isActive ? 'primary.subtle' : 'transparent'}
color={isActive ? 'primary.solid' : 'fg.muted'}
fontWeight={isActive ? 'semibold' : 'medium'}
_hover={{
bg: isActive ? 'primary.subtle' : 'bg.subtle',
textDecoration: 'none',
}}
>
<Icon as={item.icon} boxSize={5} />
<Text>{item.name}</Text>
</ChakraLink>
);
})}
</VStack>
<Box mt="auto">
<Separator mb={4} />
<ChakraLink
display="flex"
alignItems="center"
gap={3}
px={3}
py={2.5}
borderRadius="md"
textDecoration="none"
color="fg.muted"
cursor="pointer"
_hover={{
bg: 'bg.subtle',
color: 'fg.error',
textDecoration: 'none',
}}
>
<Icon as={LuLogOut} boxSize={5} />
<Text>Logout</Text>
</ChakraLink>
</Box>
</Box>
);
}

View File

@@ -9,6 +9,13 @@ export type NavItem = {
};
export const NAV_ITEMS: NavItem[] = [
{ label: "home", href: "/home", public: true },
{ label: "predictions", href: "/predictions", public: true },
{ label: "dashboard", href: "/home", public: true },
{ label: "trends", href: "/trends", public: true },
{ label: "generate", href: "/generate", public: true },
{ label: "content", href: "/content", public: true },
{ label: "sources", href: "/source-accounts", public: true },
{ label: "video", href: "/video", public: true },
{ label: "schedule", href: "/schedule", public: true },
{ label: "analytics", href: "/analytics", public: true },
{ label: "settings", href: "/settings", public: true },
];

View File

@@ -1,6 +1,6 @@
import { defineRouting } from 'next-intl/routing';
export const locales = ['en', 'tr'];
export const locales = ['en', 'tr', 'es', 'fr', 'de', 'zh', 'pt', 'ar', 'ru', 'ja'];
export const defaultLocale = 'tr';
export const routing = defineRouting({

File diff suppressed because one or more lines are too long