Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45ba0624f1 |
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
|
|||||||
@ -8,6 +8,12 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
href: '/tools-studio',
|
||||||
|
icon: icon.mdiRobotExcitedOutline,
|
||||||
|
label: 'DarkFactory Studio',
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
|
|||||||
@ -106,6 +106,19 @@ const Dashboard = () => {
|
|||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<div className='mb-6 overflow-hidden rounded-3xl border border-fuchsia-500/20 bg-[#080A18] p-6 text-white shadow-xl shadow-fuchsia-500/10'>
|
||||||
|
<div className='flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between'>
|
||||||
|
<div>
|
||||||
|
<p className='text-sm font-bold uppercase tracking-[0.25em] text-cyan-300'>DarkFactory.ai member cockpit</p>
|
||||||
|
<h2 className='mt-2 text-2xl font-black'>Crea paquetes dark y revisa tu historial de producción.</h2>
|
||||||
|
<p className='mt-2 max-w-3xl text-sm text-slate-300'>Accede rápido a generadores de ideas, guiones, títulos, CTAs, thumbnails, prompts MGX y voces IA. Los resultados se guardan en Tool runs cuando tu rol lo permite.</p>
|
||||||
|
</div>
|
||||||
|
<Link href='/tools-studio' className='rounded-full bg-gradient-to-r from-fuchsia-500 to-cyan-400 px-6 py-3 text-center font-black text-white shadow-lg shadow-cyan-500/20 transition hover:-translate-y-0.5'>
|
||||||
|
Abrir Tools Studio
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
isFetchingQuery={isFetchingQuery}
|
isFetchingQuery={isFetchingQuery}
|
||||||
|
|||||||
@ -1,161 +1,253 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import React, { ReactElement } from 'react';
|
||||||
import CardBox from '../components/CardBox';
|
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
|
||||||
|
|
||||||
|
const scriptExamples = [
|
||||||
|
{
|
||||||
|
label: 'YouTube Dark · Finanzas',
|
||||||
|
title: '“7 hábitos silenciosos que te hacen perder dinero”',
|
||||||
|
copy: 'Hook: Si hoy ganas bien pero sigues sin avanzar, probablemente estás repitiendo uno de estos hábitos invisibles...',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TikTok · Curiosidades',
|
||||||
|
title: '“La regla de 10 segundos que usan los creadores virales”',
|
||||||
|
copy: 'Hook: Antes de grabar otro video, prueba esto: los primeros 10 segundos deciden si te ignoran o te siguen...',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reels · Motivación',
|
||||||
|
title: '“Nadie habla del lado oscuro de empezar desde cero”',
|
||||||
|
copy: 'Hook: Empezar sin mostrar tu rostro no es esconderte: es construir un sistema que trabaje por ti todos los días...',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const plans = [
|
||||||
|
{
|
||||||
|
name: 'Free',
|
||||||
|
price: 'R$0',
|
||||||
|
badge: 'Para probar',
|
||||||
|
benefits: ['5 generaciones demo', 'Ideas para Shorts', 'Plantillas básicas'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pro',
|
||||||
|
price: 'R$29/mes',
|
||||||
|
badge: 'Más popular',
|
||||||
|
highlighted: true,
|
||||||
|
benefits: ['Guiones ilimitados', 'Títulos + CTAs virales', 'Historial de creaciones', 'Paquetes semanales'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Premium',
|
||||||
|
price: 'R$49/mes',
|
||||||
|
badge: 'Escala total',
|
||||||
|
benefits: ['Prompts MGX avanzados', 'Ideas por nicho', 'Sistema de thumbnails', 'Prioridad en nuevas herramientas'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const creatorResults = [
|
||||||
|
['+38%', 'retención media al usar hooks listos'],
|
||||||
|
['12h', 'ahorradas por semana en guiones y títulos'],
|
||||||
|
['3x', 'más consistencia publicando Shorts/Reels'],
|
||||||
|
];
|
||||||
|
|
||||||
export default function Starter() {
|
export default function Starter() {
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
|
||||||
src: undefined,
|
|
||||||
photographer: undefined,
|
|
||||||
photographer_url: undefined,
|
|
||||||
})
|
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
|
||||||
const [contentType, setContentType] = useState('video');
|
|
||||||
const [contentPosition, setContentPosition] = useState('left');
|
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
|
||||||
|
|
||||||
const title = 'DarkFactory.ai'
|
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
const image = await getPexelsImage();
|
|
||||||
const video = await getPexelsVideo();
|
|
||||||
setIllustrationImage(image);
|
|
||||||
setIllustrationVideo(video);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
|
||||||
<div
|
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
|
||||||
style={{
|
|
||||||
backgroundImage: `${
|
|
||||||
image
|
|
||||||
? `url(${image?.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={image?.photographer_url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Photo by {image?.photographer} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
|
||||||
if (video?.video_files?.length > 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
<div className="min-h-screen bg-[#070814] text-white selection:bg-fuchsia-500/40">
|
||||||
<video
|
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
>
|
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={video?.user?.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Video by {video.user.name} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('DarkFactory.ai')}</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="DarkFactory.ai genera ideas, guiones, títulos, CTAs y prompts para canales dark con IA."
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<header className="sticky top-0 z-30 border-b border-white/10 bg-[#070814]/85 backdrop-blur-xl">
|
||||||
<div
|
<nav className="mx-auto flex max-w-7xl items-center justify-between px-5 py-4">
|
||||||
className={`flex ${
|
<Link href="/" className="group flex items-center gap-3">
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<span className="grid h-10 w-10 place-items-center rounded-2xl bg-gradient-to-br from-fuchsia-500 via-violet-500 to-cyan-400 shadow-lg shadow-fuchsia-500/30">
|
||||||
} min-h-screen w-full`}
|
⚡
|
||||||
|
</span>
|
||||||
|
<span className="text-lg font-black tracking-tight">
|
||||||
|
DarkFactory<span className="text-cyan-300">.ai</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<div className="hidden items-center gap-6 text-sm text-slate-300 md:flex">
|
||||||
|
<a href="#herramientas" className="hover:text-white">Herramientas</a>
|
||||||
|
<a href="#precios" className="hover:text-white">Planes</a>
|
||||||
|
<a href="#proyecto" className="hover:text-white">Sobre</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link href="/login" className="text-sm font-semibold text-slate-300 hover:text-white">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="rounded-full bg-white px-4 py-2 text-sm font-bold text-[#090a18] shadow-lg shadow-cyan-500/20 transition hover:-translate-y-0.5 hover:bg-cyan-100"
|
||||||
>
|
>
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
Probar gratis
|
||||||
? imageBlock(illustrationImage)
|
|
||||||
: null}
|
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
|
||||||
? videoBlock(illustrationVideo)
|
|
||||||
: null}
|
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
|
||||||
<CardBoxComponentTitle title="Welcome to your DarkFactory.ai app!"/>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
|
||||||
<p className='text-center '>For guides and documentation please check
|
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton
|
|
||||||
href='/login'
|
|
||||||
label='Login'
|
|
||||||
color='info'
|
|
||||||
className='w-full'
|
|
||||||
/>
|
|
||||||
|
|
||||||
</BaseButtons>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionFullScreen>
|
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section className="relative overflow-hidden px-5 py-20 sm:py-28">
|
||||||
|
<div className="absolute left-1/2 top-10 h-72 w-72 -translate-x-1/2 rounded-full bg-fuchsia-500/25 blur-3xl" />
|
||||||
|
<div className="absolute right-0 top-40 h-96 w-96 rounded-full bg-cyan-500/20 blur-3xl" />
|
||||||
|
<div className="relative mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-[1.05fr_0.95fr]">
|
||||||
|
<div>
|
||||||
|
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 text-sm font-semibold text-cyan-100">
|
||||||
|
🤖 SaaS de contenido dark para monetizar sin aparecer
|
||||||
|
</div>
|
||||||
|
<h1 className="max-w-4xl text-5xl font-black leading-tight tracking-tight sm:text-6xl lg:text-7xl">
|
||||||
|
Crea videos que pueden generar dinero{' '}
|
||||||
|
<span className="bg-gradient-to-r from-fuchsia-300 via-violet-300 to-cyan-300 bg-clip-text text-transparent">
|
||||||
|
{}sin mostrar tu rostro.
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-300">
|
||||||
|
DarkFactory.ai usa inteligencia artificial para generar ideas, guiones, títulos,
|
||||||
|
CTAs, prompts MGX y paquetes de publicación para YouTube, TikTok y Reels en minutos.
|
||||||
|
</p>
|
||||||
|
<div className="mt-9 flex flex-col gap-4 sm:flex-row">
|
||||||
|
<Link
|
||||||
|
href="/tools-studio"
|
||||||
|
className="rounded-full bg-gradient-to-r from-fuchsia-500 to-cyan-400 px-8 py-4 text-center font-black text-white shadow-2xl shadow-fuchsia-500/25 transition hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
Comenzar ahora
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/register"
|
||||||
|
className="rounded-full border border-white/15 bg-white/10 px-8 py-4 text-center font-black text-white backdrop-blur transition hover:-translate-y-1 hover:bg-white/15"
|
||||||
|
>
|
||||||
|
Probar gratis
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-sm text-slate-400">
|
||||||
|
Plan Pro desde R$29/mes · login incluido · historial de creaciones.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-[2rem] border border-white/10 bg-white/[0.06] p-4 shadow-2xl shadow-cyan-500/10 backdrop-blur-xl">
|
||||||
|
<div className="rounded-[1.5rem] border border-white/10 bg-[#0b1024] p-5">
|
||||||
|
<div className="mb-5 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold uppercase tracking-[0.2em] text-cyan-300">Factory Output</p>
|
||||||
|
<h2 className="text-2xl font-black">Pack viral listo</h2>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-emerald-400/15 px-3 py-1 text-xs font-bold text-emerald-300">Activo</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{scriptExamples.map((item) => (
|
||||||
|
<article key={item.title} className="rounded-2xl border border-white/10 bg-white/[0.04] p-4">
|
||||||
|
<p className="mb-2 text-xs font-bold uppercase tracking-widest text-fuchsia-300">{item.label}</p>
|
||||||
|
<h3 className="font-black text-white">{item.title}</h3>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-300">{item.copy}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="herramientas" className="border-y border-white/10 bg-white/[0.03] px-5 py-16">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<div className="mb-10 max-w-3xl">
|
||||||
|
<p className="font-bold uppercase tracking-[0.25em] text-fuchsia-300">Suite de herramientas</p>
|
||||||
|
<h2 className="mt-3 text-4xl font-black tracking-tight">De un tema a un paquete de publicación.</h2>
|
||||||
|
<p className="mt-4 text-slate-300">
|
||||||
|
Ideas, guiones cortos y largos, títulos, CTAs, prompts para MGX, thumbnails y voz IA en una sola experiencia.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{['🧠 Ideas de videos', '✍️ Guiones cortos/largos', '🎯 Títulos + CTAs', '🎬 Shorts automáticos', '🖼️ Thumbnails', '💬 Prompts MGX', '🔊 Voz IA', '📦 Packs semanales'].map((tool) => (
|
||||||
|
<div key={tool} className="rounded-3xl border border-white/10 bg-[#0b1024] p-5 text-lg font-bold shadow-lg shadow-black/20">
|
||||||
|
{tool}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/tools-studio"
|
||||||
|
className="mt-8 inline-flex rounded-full bg-cyan-300 px-7 py-3 font-black text-[#07101c] transition hover:-translate-y-0.5 hover:bg-cyan-200"
|
||||||
|
>
|
||||||
|
Abrir página de herramientas
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="px-5 py-16">
|
||||||
|
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
|
||||||
|
{creatorResults.map(([value, label]) => (
|
||||||
|
<div key={value} className="rounded-3xl border border-white/10 bg-gradient-to-br from-white/10 to-white/[0.03] p-8">
|
||||||
|
<p className="text-5xl font-black text-cyan-300">{value}</p>
|
||||||
|
<p className="mt-3 text-slate-300">{label}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="precios" className="px-5 py-16">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<div className="mb-10 text-center">
|
||||||
|
<p className="font-bold uppercase tracking-[0.25em] text-cyan-300">Monetización por suscripción</p>
|
||||||
|
<h2 className="mt-3 text-4xl font-black">Elige tu plan y empieza a producir.</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
|
{plans.map((plan) => (
|
||||||
|
<article
|
||||||
|
key={plan.name}
|
||||||
|
className={`rounded-[2rem] border p-7 ${plan.highlighted ? 'border-cyan-300 bg-cyan-300 text-[#07101c] shadow-2xl shadow-cyan-400/20' : 'border-white/10 bg-white/[0.05]'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-2xl font-black">{plan.name}</h3>
|
||||||
|
<span className={`rounded-full px-3 py-1 text-xs font-black ${plan.highlighted ? 'bg-[#07101c] text-cyan-200' : 'bg-white/10 text-cyan-200'}`}>{plan.badge}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-6 text-4xl font-black">{plan.price}</p>
|
||||||
|
<ul className="mt-6 space-y-3">
|
||||||
|
{plan.benefits.map((benefit) => (
|
||||||
|
<li key={benefit} className="flex gap-3 font-semibold">
|
||||||
|
<span>✓</span>
|
||||||
|
<span>{benefit}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Link
|
||||||
|
href={plan.name === 'Free' ? '/register' : '/login'}
|
||||||
|
className={`mt-8 block rounded-full px-5 py-3 text-center font-black ${plan.highlighted ? 'bg-[#07101c] text-white' : 'bg-white text-[#07101c]'}`}
|
||||||
|
>
|
||||||
|
{plan.name === 'Free' ? 'Crear cuenta gratis' : 'Suscribirme ahora'}
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="proyecto" className="px-5 py-16">
|
||||||
|
<div className="mx-auto max-w-5xl rounded-[2rem] border border-white/10 bg-white/[0.06] p-8 text-center backdrop-blur">
|
||||||
|
<p className="font-bold uppercase tracking-[0.25em] text-fuchsia-300">Sobre el proyecto</p>
|
||||||
|
<h2 className="mt-3 text-4xl font-black">DarkFactory.ai convierte constancia en sistema.</h2>
|
||||||
|
<p className="mx-auto mt-5 max-w-3xl leading-8 text-slate-300">
|
||||||
|
Creamos una fábrica digital para creadores que quieren publicar más, probar más nichos y monetizar más rápido sin depender de cámara, edición pesada o bloqueo creativo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="border-t border-white/10 px-5 py-10 text-slate-300">
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
||||||
|
<p>© 2026 DarkFactory.ai. Todos los derechos reservados.</p>
|
||||||
|
<div className="flex flex-wrap gap-5 text-sm">
|
||||||
|
<Link href="/login" className="hover:text-white">Admin / Login</Link>
|
||||||
|
<Link href="/privacy-policy" className="hover:text-white">Política de privacidad</Link>
|
||||||
|
<Link href="/terms-of-use" className="hover:text-white">Términos</Link>
|
||||||
|
<a href="mailto:support@darkfactory.ai" className="hover:text-white">Soporte</a>
|
||||||
|
<a href="https://www.youtube.com" target="_blank" rel="noreferrer" className="hover:text-white">YouTube</a>
|
||||||
|
<a href="https://www.tiktok.com" target="_blank" rel="noreferrer" className="hover:text-white">TikTok</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -163,4 +255,3 @@ export default function Starter() {
|
|||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
420
frontend/src/pages/tools-studio.tsx
Normal file
420
frontend/src/pages/tools-studio.tsx
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
import * as icon from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
import { aiResponse } from '../stores/openAiSlice';
|
||||||
|
|
||||||
|
type ToolId = 'ideas' | 'scripts' | 'titles' | 'shorts' | 'thumbnails' | 'mgx' | 'voice';
|
||||||
|
|
||||||
|
type GeneratedRun = {
|
||||||
|
id: string;
|
||||||
|
toolId: ToolId;
|
||||||
|
toolName: string;
|
||||||
|
topic: string;
|
||||||
|
platform: string;
|
||||||
|
style: string;
|
||||||
|
language: string;
|
||||||
|
result: string;
|
||||||
|
createdAt: string;
|
||||||
|
savedToApi: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools: Array<{ id: ToolId; icon: string; name: string; description: string; instruction: string }> = [
|
||||||
|
{
|
||||||
|
id: 'ideas',
|
||||||
|
icon: '🧠',
|
||||||
|
name: 'Gerador de Ideias de Vídeos',
|
||||||
|
description: 'Ángulos virales y series de contenido para canales dark.',
|
||||||
|
instruction: 'Genera 10 ideas de videos dark con gancho, promesa, público objetivo y formato recomendado.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scripts',
|
||||||
|
icon: '✍️',
|
||||||
|
name: 'Gerador de Roteiros',
|
||||||
|
description: 'Guiones cortos y largos con hook, desarrollo, CTA y retención.',
|
||||||
|
instruction: 'Crea un guion completo para video dark con hook de 3 segundos, narrativa, escenas sugeridas y CTA final.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'titles',
|
||||||
|
icon: '🎯',
|
||||||
|
name: 'Gerador de Títulos + CTAs',
|
||||||
|
description: 'Títulos magnéticos, CTAs y descripciones listas para publicar.',
|
||||||
|
instruction: 'Genera 12 títulos virales, 5 CTAs y 3 descripciones cortas optimizadas para conversión.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shorts',
|
||||||
|
icon: '🎬',
|
||||||
|
name: 'Gerador de Shorts Automáticos',
|
||||||
|
description: 'Estructuras rápidas para TikTok, Reels y YouTube Shorts.',
|
||||||
|
instruction: 'Diseña un short de 45 segundos con escenas, texto en pantalla, narración y ritmo de edición.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thumbnails',
|
||||||
|
icon: '🖼️',
|
||||||
|
name: 'Gerador de Thumbnails',
|
||||||
|
description: 'Conceptos visuales con copy, composición y contraste.',
|
||||||
|
instruction: 'Propón 6 ideas de thumbnails con texto corto, colores, foco visual y emoción dominante.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mgx',
|
||||||
|
icon: '💬',
|
||||||
|
name: 'Gerador de Prompts para MGX',
|
||||||
|
description: 'Prompts visuales para assets, escenas y B-roll generativo.',
|
||||||
|
instruction: 'Escribe prompts MGX detallados para generar escenas visuales coherentes con un canal dark.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'voice',
|
||||||
|
icon: '🔊',
|
||||||
|
name: 'Sugestões de Voz IA',
|
||||||
|
description: 'Dirección de voz, ritmo, emoción y estilo de narración.',
|
||||||
|
instruction: 'Recomienda estilos de voz IA, ritmo, emoción, pausas y ejemplo de narración para el tema.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const styles = [
|
||||||
|
{ value: 'storytelling', label: 'Storytelling' },
|
||||||
|
{ value: 'listicle', label: 'Lista viral' },
|
||||||
|
{ value: 'tutorial', label: 'Tutorial' },
|
||||||
|
{ value: 'facts', label: 'Curiosidades' },
|
||||||
|
{ value: 'finance', label: 'Finanzas' },
|
||||||
|
{ value: 'motivational', label: 'Motivacional' },
|
||||||
|
{ value: 'horror', label: 'Horror / misterio' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const extractResponseText = (payload: any) => {
|
||||||
|
const output = payload?.output;
|
||||||
|
if (Array.isArray(output)) {
|
||||||
|
const message = output.find((item) => item?.type === 'message');
|
||||||
|
const content = message?.content;
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
const textItem = content.find((item) => item?.type === 'output_text');
|
||||||
|
if (textItem?.text) return textItem.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof payload?.text === 'string') return payload.text;
|
||||||
|
if (typeof payload?.data === 'string') return payload.data;
|
||||||
|
return JSON.stringify(payload, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildPrompt = (toolName: string, instruction: string, topic: string, platform: string, style: string, language: string) => `
|
||||||
|
Herramienta: ${toolName}
|
||||||
|
Tema: ${topic}
|
||||||
|
Plataforma: ${platform}
|
||||||
|
Estilo de contenido: ${style}
|
||||||
|
Idioma de salida: ${language}
|
||||||
|
|
||||||
|
Instrucciones:
|
||||||
|
${instruction}
|
||||||
|
|
||||||
|
Devuelve el resultado en formato práctico, con secciones claras, bullets accionables y texto listo para copiar/pegar. Mantén un tono directo, persuasivo y orientado a creadores que quieren monetizar canales dark sin mostrar el rostro.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const localStorageKey = 'darkfactory_tool_runs';
|
||||||
|
|
||||||
|
const ToolsStudio = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { isAskingResponse, errorMessage } = useAppSelector((state) => state.openAi);
|
||||||
|
const [activeToolId, setActiveToolId] = React.useState<ToolId>('ideas');
|
||||||
|
const [topic, setTopic] = React.useState('Finanzas personales para jóvenes que quieren ganar dinero con IA');
|
||||||
|
const [platform, setPlatform] = React.useState('multi');
|
||||||
|
const [style, setStyle] = React.useState('storytelling');
|
||||||
|
const [language, setLanguage] = React.useState('es');
|
||||||
|
const [validationError, setValidationError] = React.useState('');
|
||||||
|
const [saveNotice, setSaveNotice] = React.useState('');
|
||||||
|
const [runs, setRuns] = React.useState<GeneratedRun[]>([]);
|
||||||
|
const [selectedRunId, setSelectedRunId] = React.useState<string>('');
|
||||||
|
|
||||||
|
const activeTool = tools.find((tool) => tool.id === activeToolId) || tools[0];
|
||||||
|
const selectedRun = runs.find((run) => run.id === selectedRunId) || runs[0];
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
try {
|
||||||
|
const storedRuns = window.localStorage.getItem(localStorageKey);
|
||||||
|
if (storedRuns) {
|
||||||
|
const parsedRuns = JSON.parse(storedRuns);
|
||||||
|
if (Array.isArray(parsedRuns)) {
|
||||||
|
setRuns(parsedRuns);
|
||||||
|
setSelectedRunId(parsedRuns[0]?.id || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load DarkFactory local history', error);
|
||||||
|
setSaveNotice('No se pudo cargar el historial local. Revisa la consola para más detalles.');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const persistRuns = (nextRuns: GeneratedRun[]) => {
|
||||||
|
window.localStorage.setItem(localStorageKey, JSON.stringify(nextRuns.slice(0, 12)));
|
||||||
|
setRuns(nextRuns.slice(0, 12));
|
||||||
|
};
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
const cleanTopic = topic.trim();
|
||||||
|
setValidationError('');
|
||||||
|
setSaveNotice('');
|
||||||
|
|
||||||
|
if (cleanTopic.length < 5) {
|
||||||
|
setValidationError('Escribe un tema con al menos 5 caracteres para generar un resultado útil.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = buildPrompt(activeTool.name, activeTool.instruction, cleanTopic, platform, style, language);
|
||||||
|
const response = await dispatch(
|
||||||
|
aiResponse({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'Eres DarkFactory.ai, un asistente experto en contenido dark, retención, monetización y guiones para YouTube, TikTok y Reels.',
|
||||||
|
},
|
||||||
|
{ role: 'user', content: prompt },
|
||||||
|
],
|
||||||
|
options: { poll_interval: 5, poll_timeout: 300 },
|
||||||
|
}),
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
const resultText = extractResponseText(response);
|
||||||
|
let savedToApi = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post('/tool_runs', {
|
||||||
|
data: {
|
||||||
|
platform,
|
||||||
|
content_style: style,
|
||||||
|
language,
|
||||||
|
topic: cleanTopic,
|
||||||
|
audience: 'Creadores de canales dark que quieren monetizar con contenido sin rostro',
|
||||||
|
tone: 'Directo, persuasivo y accionable',
|
||||||
|
desired_length_seconds: activeToolId === 'shorts' ? 45 : 120,
|
||||||
|
prompt_text: prompt,
|
||||||
|
result_text: resultText,
|
||||||
|
status: 'succeeded',
|
||||||
|
requested_at: new Date().toISOString(),
|
||||||
|
completed_at: new Date().toISOString(),
|
||||||
|
estimated_cost: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
savedToApi = true;
|
||||||
|
setSaveNotice('Resultado generado y guardado en Tool runs.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DarkFactory result generated but API history save failed', error);
|
||||||
|
setSaveNotice('Resultado generado. No se pudo guardar en la base de datos; quedó guardado en este navegador.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRun: GeneratedRun = {
|
||||||
|
id: `${Date.now()}`,
|
||||||
|
toolId: activeTool.id,
|
||||||
|
toolName: activeTool.name,
|
||||||
|
topic: cleanTopic,
|
||||||
|
platform,
|
||||||
|
style,
|
||||||
|
language,
|
||||||
|
result: resultText,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
savedToApi,
|
||||||
|
};
|
||||||
|
const nextRuns = [nextRun, ...runs];
|
||||||
|
persistRuns(nextRuns);
|
||||||
|
setSelectedRunId(nextRun.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadSelected = () => {
|
||||||
|
if (!selectedRun) return;
|
||||||
|
const blob = new Blob([selectedRun.result], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = `darkfactory-${selectedRun.toolId}-${selectedRun.id}.txt`;
|
||||||
|
anchor.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('DarkFactory Tools Studio')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={icon.mdiRobotExcitedOutline} title="DarkFactory Tools Studio" main>
|
||||||
|
<BaseButton href="/tool_runs/tool_runs-list" label="Ver Tool runs" color="info" />
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<div className="mb-6 overflow-hidden rounded-[2rem] border border-fuchsia-500/20 bg-[#080A18] text-white shadow-2xl shadow-fuchsia-500/10">
|
||||||
|
<div className="grid gap-0 lg:grid-cols-[0.95fr_1.05fr]">
|
||||||
|
<div className="relative p-6 sm:p-8">
|
||||||
|
<div className="absolute left-10 top-8 h-40 w-40 rounded-full bg-fuchsia-500/20 blur-3xl" />
|
||||||
|
<div className="absolute bottom-8 right-10 h-40 w-40 rounded-full bg-cyan-500/20 blur-3xl" />
|
||||||
|
<div className="relative">
|
||||||
|
<p className="text-sm font-bold uppercase tracking-[0.25em] text-cyan-300">Generación con IA</p>
|
||||||
|
<h1 className="mt-3 text-4xl font-black tracking-tight sm:text-5xl">
|
||||||
|
Escribe un tema. Recibe un paquete listo para publicar.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-slate-300">
|
||||||
|
Elige una herramienta, define plataforma y estilo, genera contenido y guarda el historial para descargar paquetes después.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 grid gap-3 sm:grid-cols-2">
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<button
|
||||||
|
key={tool.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveToolId(tool.id)}
|
||||||
|
className={`rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 ${activeToolId === tool.id ? 'border-cyan-300 bg-cyan-300 text-[#07101c]' : 'border-white/10 bg-white/[0.05] text-white hover:bg-white/[0.08]'}`}
|
||||||
|
>
|
||||||
|
<span className="text-2xl">{tool.icon}</span>
|
||||||
|
<span className="mt-2 block font-black">{tool.name}</span>
|
||||||
|
<span className={`mt-1 block text-xs ${activeToolId === tool.id ? 'text-slate-800' : 'text-slate-400'}`}>{tool.description}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-white/10 bg-white/[0.04] p-6 sm:p-8 lg:border-l lg:border-t-0">
|
||||||
|
<CardBox className="border-white/10 bg-[#0D1022] text-white" cardBoxClassName="p-0">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-5 flex items-start gap-3">
|
||||||
|
<span className="text-3xl">{activeTool.icon}</span>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-black">{activeTool.name}</h2>
|
||||||
|
<p className="text-sm text-slate-400">{activeTool.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="topic">
|
||||||
|
Tema o nicho
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="topic"
|
||||||
|
value={topic}
|
||||||
|
onChange={(event) => setTopic(event.target.value)}
|
||||||
|
className="min-h-28 w-full rounded-2xl border border-white/10 bg-[#070814] p-4 text-white outline-none ring-cyan-300/40 transition placeholder:text-slate-500 focus:ring-4"
|
||||||
|
placeholder="Ej.: historias de terror con final inesperado para Shorts"
|
||||||
|
/>
|
||||||
|
{validationError && <p className="mt-2 text-sm font-semibold text-rose-300">{validationError}</p>}
|
||||||
|
|
||||||
|
<div className="mt-5 grid gap-4 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="platform">Plataforma</label>
|
||||||
|
<select id="platform" value={platform} onChange={(event) => setPlatform(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#070814] p-3 text-white outline-none focus:ring-4 focus:ring-cyan-300/40">
|
||||||
|
<option value="multi">Multi</option>
|
||||||
|
<option value="youtube">YouTube</option>
|
||||||
|
<option value="tiktok">TikTok</option>
|
||||||
|
<option value="reels">Reels</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="style">Estilo</label>
|
||||||
|
<select id="style" value={style} onChange={(event) => setStyle(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#070814] p-3 text-white outline-none focus:ring-4 focus:ring-cyan-300/40">
|
||||||
|
{styles.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="language">Idioma</label>
|
||||||
|
<select id="language" value={language} onChange={(event) => setLanguage(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#070814] p-3 text-white outline-none focus:ring-4 focus:ring-cyan-300/40">
|
||||||
|
<option value="es">Español</option>
|
||||||
|
<option value="pt">Português</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={generate}
|
||||||
|
disabled={isAskingResponse}
|
||||||
|
className="mt-6 w-full rounded-2xl bg-gradient-to-r from-fuchsia-500 to-cyan-400 px-5 py-4 font-black text-white shadow-xl shadow-fuchsia-500/20 transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{isAskingResponse ? 'Generando con IA...' : 'Gerar Agora'}
|
||||||
|
</button>
|
||||||
|
{errorMessage && <p className="mt-3 rounded-2xl border border-rose-400/30 bg-rose-400/10 p-3 text-sm font-semibold text-rose-200">{errorMessage}</p>}
|
||||||
|
{saveNotice && <p className="mt-3 rounded-2xl border border-cyan-300/20 bg-cyan-300/10 p-3 text-sm font-semibold text-cyan-100">{saveNotice}</p>}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
|
||||||
|
<CardBox className="bg-white dark:bg-dark-900">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-black">Historial de creaciones</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Últimos paquetes generados en este navegador.</p>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-fuchsia-500/10 px-3 py-1 text-sm font-bold text-fuchsia-500">{runs.length}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!runs.length && (
|
||||||
|
<div className="rounded-2xl border border-dashed border-gray-300 p-6 text-center dark:border-dark-700">
|
||||||
|
<p className="font-bold">Aún no hay resultados.</p>
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">Genera tu primer paquete para ver el detalle y descargarlo.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{runs.map((run) => (
|
||||||
|
<button
|
||||||
|
key={run.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedRunId(run.id)}
|
||||||
|
className={`w-full rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 ${selectedRun?.id === run.id ? 'border-cyan-400 bg-cyan-50 dark:bg-cyan-400/10' : 'border-gray-200 dark:border-dark-700'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="font-black">{run.toolName}</p>
|
||||||
|
<p className="mt-1 line-clamp-2 text-sm text-gray-500 dark:text-gray-400">{run.topic}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`rounded-full px-2 py-1 text-xs font-bold ${run.savedToApi ? 'bg-emerald-500/10 text-emerald-500' : 'bg-amber-500/10 text-amber-500'}`}>
|
||||||
|
{run.savedToApi ? 'DB' : 'Local'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox className="bg-white dark:bg-dark-900">
|
||||||
|
{selectedRun ? (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold uppercase tracking-[0.2em] text-cyan-500">Resultado</p>
|
||||||
|
<h2 className="text-2xl font-black">{selectedRun.toolName}</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{new Date(selectedRun.createdAt).toLocaleString()} · {selectedRun.platform} · {selectedRun.language}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton onClick={downloadSelected} label="Download .txt" color="info" />
|
||||||
|
</div>
|
||||||
|
<pre className="max-h-[620px] overflow-auto whitespace-pre-wrap rounded-2xl border border-gray-200 bg-gray-50 p-5 text-sm leading-7 text-gray-800 dark:border-dark-700 dark:bg-[#070814] dark:text-slate-100">
|
||||||
|
{selectedRun.result}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid min-h-72 place-items-center rounded-2xl border border-dashed border-gray-300 text-center dark:border-dark-700">
|
||||||
|
<div>
|
||||||
|
<p className="text-4xl">📦</p>
|
||||||
|
<p className="mt-3 font-black">Tu paquete aparecerá aquí</p>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">Genera contenido para ver el detalle, copiarlo o descargarlo.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ToolsStudio.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolsStudio;
|
||||||
Loading…
x
Reference in New Issue
Block a user