The npm install command failed in my project. Analyze the following er

This commit is contained in:
Vadym 2025-07-03 14:37:19 +00:00
parent 3696b98582
commit 5a067c16ac
20 changed files with 949 additions and 53 deletions

0
.modified Normal file
View file

22
docs/blueprint.md Normal file
View file

@ -0,0 +1,22 @@
# **App Name**: ContentFlow
## Core Features:
- User Authentication: Login and authentication using email or social accounts (Firebase/Auth.js).
- Briefing Form: Briefing form for users to input their content requirements; an AI chat with OpenRouter will come later.
- Content Plan Generation: AI-powered tool to generate a content plan based on the user's brief via a webhook that connects to n8n and GPT.
- Content Plan Editor: Display the AI-generated content plan for user review and edits.
- Content Generation: AI tool to generate content based on the approved content plan via n8n.
- Content Editor: Content preview and editor for the generated content, so the user can approve and modify.
- Content Export: Export content plan and generated content as a PDF or ZIP file.
- Storage: Firestore is used to store all brief data, plans, and generated content in a structured, user-specific hierarchy.
## Style Guidelines:
- Primary color: Deep indigo (#3F51B5), a professional and calming color to reflect the AI's ability to focus.
- Background color: Very light grey (#F0F2F5), a neutral backdrop that keeps the interface feeling airy.
- Accent color: Electric purple (#BF5AF2), adding a zing that evokes the spark of creativity.
- Body and headline font: 'Inter' sans-serif for a modern, neutral look; suitable for both headlines and body text.
- Code font: 'Source Code Pro' for any code snippets displayed.
- Simple and modern icons from Headless UI and ShadCN to maintain a clean interface.
- Subtle transitions and progress bars to provide feedback during AI operations and content loading.

43
package-lock.json generated
View file

@ -38,6 +38,7 @@
"dotenv": "^16.5.0",
"embla-carousel-react": "^8.6.0",
"firebase": "^11.9.1",
"framer-motion": "^11.3.12",
"genkit": "^1.13.0",
"lucide-react": "^0.475.0",
"next": "15.3.3",
@ -5958,6 +5959,33 @@
"node": ">= 0.6"
}
},
"node_modules/framer-motion": {
"version": "11.18.2",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
"integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^11.18.1",
"motion-utils": "^11.18.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -7330,6 +7358,21 @@
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
"license": "MIT"
},
"node_modules/motion-dom": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
"integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
"license": "MIT",
"dependencies": {
"motion-utils": "^11.18.1"
}
},
"node_modules/motion-utils": {
"version": "11.18.1",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
"integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",

View file

@ -42,6 +42,7 @@
"dotenv": "^16.5.0",
"embla-carousel-react": "^8.6.0",
"firebase": "^11.9.1",
"framer-motion": "^11.3.12",
"genkit": "^1.13.0",
"lucide-react": "^0.475.0",
"next": "15.3.3",

View file

@ -1 +1,5 @@
// Flows will be imported for their side effects in this file.
import { config } from 'dotenv';
config();
import '@/ai/flows/generate-content.ts';
import '@/ai/flows/generate-content-plan.ts';

View file

@ -0,0 +1,47 @@
'use server';
/**
* @fileOverview AI flow to generate a content plan based on user brief.
*
* - generateContentPlan - The main function to generate a content plan.
* - GenerateContentPlanInput - The input type for the generateContentPlan function.
* - GenerateContentPlanOutput - The output type for the generateContentPlan function.
*/
import {ai} from '@/ai/genkit';
import {z} from 'genkit';
const GenerateContentPlanInputSchema = z.object({
brief: z.string().describe('The content brief provided by the user.'),
});
export type GenerateContentPlanInput = z.infer<typeof GenerateContentPlanInputSchema>;
const GenerateContentPlanOutputSchema = z.object({
contentPlan: z.string().describe('The generated content plan.'),
});
export type GenerateContentPlanOutput = z.infer<typeof GenerateContentPlanOutputSchema>;
export async function generateContentPlan(input: GenerateContentPlanInput): Promise<GenerateContentPlanOutput> {
return generateContentPlanFlow(input);
}
const generateContentPlanPrompt = ai.definePrompt({
name: 'generateContentPlanPrompt',
input: {schema: GenerateContentPlanInputSchema},
output: {schema: GenerateContentPlanOutputSchema},
prompt: `You are an AI content planning assistant. Generate a detailed content plan based on the user's brief.
Brief: {{{brief}}}`,
});
const generateContentPlanFlow = ai.defineFlow(
{
name: 'generateContentPlanFlow',
inputSchema: GenerateContentPlanInputSchema,
outputSchema: GenerateContentPlanOutputSchema,
},
async input => {
const {output} = await generateContentPlanPrompt(input);
return output!;
}
);

View file

@ -0,0 +1,49 @@
'use server';
/**
* @fileOverview Generates content based on an approved content plan.
*
* - generateContent - A function that generates content from a content plan.
* - GenerateContentInput - The input type for the generateContent function.
* - GenerateContentOutput - The return type for the generateContent function.
*/
import {ai} from '@/ai/genkit';
import {z} from 'genkit';
const GenerateContentInputSchema = z.object({
contentPlan: z
.string()
.describe('The approved content plan to generate content from.'),
});
export type GenerateContentInput = z.infer<typeof GenerateContentInputSchema>;
const GenerateContentOutputSchema = z.object({
generatedContent: z
.string()
.describe('The content generated based on the content plan.'),
});
export type GenerateContentOutput = z.infer<typeof GenerateContentOutputSchema>;
export async function generateContent(input: GenerateContentInput): Promise<GenerateContentOutput> {
return generateContentFlow(input);
}
const prompt = ai.definePrompt({
name: 'generateContentPrompt',
input: {schema: GenerateContentInputSchema},
output: {schema: GenerateContentOutputSchema},
prompt: `You are an AI content generator. Generate content based on the following content plan:\n\nContent Plan: {{{contentPlan}}}`,
});
const generateContentFlow = ai.defineFlow(
{
name: 'generateContentFlow',
inputSchema: GenerateContentInputSchema,
outputSchema: GenerateContentOutputSchema,
},
async input => {
const {output} = await prompt(input);
return output!;
}
);

View file

@ -0,0 +1,143 @@
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { BotMessageSquare } from 'lucide-react';
const briefSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters long.'),
goal: z.string().min(10, 'Content goal must be at least 10 characters long.'),
audience: z.string().min(3, 'Target audience must be at least 3 characters long.'),
points: z.string().min(10, 'Please provide some key points.'),
tone: z.string().min(3, 'Tone of voice must be at least 3 characters long.'),
keywords: z.string().optional(),
});
export type BriefData = z.infer<typeof briefSchema>;
interface BriefFormProps {
onSubmit: (data: BriefData) => void;
isLoading: boolean;
}
export function BriefForm({ onSubmit, isLoading }: BriefFormProps) {
const form = useForm<BriefData>({
resolver: zodResolver(briefSchema),
defaultValues: {
title: '',
goal: '',
audience: '',
points: '',
tone: '',
keywords: '',
},
});
return (
<Card>
<CardHeader>
<CardTitle>Content Brief</CardTitle>
<CardDescription>Fill out the details below to give the AI context for your content.</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Project Title</FormLabel>
<FormControl>
<Input placeholder="e.g., Q4 Social Media Campaign" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="goal"
render={({ field }) => (
<FormItem>
<FormLabel>What is the primary goal of this content?</FormLabel>
<FormControl>
<Input placeholder="e.g., To drive traffic to our new feature page" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="audience"
render={({ field }) => (
<FormItem>
<FormLabel>Who is the target audience?</FormLabel>
<FormControl>
<Input placeholder="e.g., Tech-savvy project managers, aged 25-40" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="points"
render={({ field }) => (
<FormItem>
<FormLabel>Key Talking Points</FormLabel>
<FormControl>
<Textarea placeholder="List the main ideas, arguments, or features to include. Use bullet points or short sentences." {...field} rows={5} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<FormField
control={form.control}
name="tone"
render={({ field }) => (
<FormItem>
<FormLabel>Tone of Voice</FormLabel>
<FormControl>
<Input placeholder="e.g., Professional yet approachable" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keywords"
render={({ field }) => (
<FormItem>
<FormLabel>Keywords to Include (optional)</FormLabel>
<FormControl>
<Input placeholder="e.g., AI content, productivity, automation" {...field} />
</FormControl>
<FormDescription>Separate keywords with commas.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end">
<Button type="submit" disabled={isLoading}>
<BotMessageSquare className="mr-2 h-4 w-4" />
Generate Content Plan
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
);
}

View file

@ -0,0 +1,50 @@
'use client';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
import { ArrowLeft, Download, Save } from 'lucide-react';
interface ContentEditorProps {
content: string;
setContent: (content: string) => void;
onEditPlan: () => void;
}
export function ContentEditor({ content, setContent, onEditPlan }: ContentEditorProps) {
return (
<Card>
<CardHeader>
<CardTitle>Generated Content</CardTitle>
<CardDescription>
Your content is ready! Make final adjustments and export when you are happy with the result.
</CardDescription>
</CardHeader>
<CardContent>
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
rows={20}
className="text-base"
placeholder="Final content will appear here..."
/>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" onClick={onEditPlan}>
<ArrowLeft className="mr-2 h-4 w-4" />
Edit Plan
</Button>
<div className="flex gap-2">
<Button variant="secondary">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
<Button>
<Save className="mr-2 h-4 w-4" />
Save
</Button>
</div>
</CardFooter>
</Card>
);
}

View file

@ -0,0 +1,52 @@
'use client';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
import { ArrowLeft, Download, Sparkles } from 'lucide-react';
interface PlanEditorProps {
plan: string;
setPlan: (plan: string) => void;
onApprove: () => void;
onEditBrief: () => void;
isLoading: boolean;
}
export function PlanEditor({ plan, setPlan, onApprove, onEditBrief, isLoading }: PlanEditorProps) {
return (
<Card>
<CardHeader>
<CardTitle>Content Plan</CardTitle>
<CardDescription>
Here is the AI-generated plan. Review and make any necessary edits before generating the final content.
</CardDescription>
</CardHeader>
<CardContent>
<Textarea
value={plan}
onChange={(e) => setPlan(e.target.value)}
rows={15}
className="font-code text-sm"
placeholder="Content plan will appear here..."
/>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" onClick={onEditBrief} disabled={isLoading}>
<ArrowLeft className="mr-2 h-4 w-4" />
Edit Brief
</Button>
<div className="flex gap-2">
<Button variant="secondary" disabled={isLoading}>
<Download className="mr-2 h-4 w-4" />
Export
</Button>
<Button onClick={onApprove} disabled={isLoading}>
<Sparkles className="mr-2 h-4 w-4" />
Approve & Generate Content
</Button>
</div>
</CardFooter>
</Card>
);
}

View file

@ -0,0 +1,75 @@
'use client';
import { cn } from '@/lib/utils';
import { Check } from 'lucide-react';
type Step = 'brief' | 'plan' | 'content';
const steps: Step[] = ['brief', 'plan', 'content'];
const stepLabels: Record<Step, string> = {
brief: 'Brief',
plan: 'Content Plan',
content: 'Generated Content',
};
interface StepperProps {
currentStep: Step;
}
export function Stepper({ currentStep }: StepperProps) {
const currentStepIndex = steps.indexOf(currentStep);
return (
<nav aria-label="Progress">
<ol role="list" className="flex items-center">
{steps.map((step, stepIdx) => (
<li key={step} className={cn('relative', { 'pr-8 sm:pr-20': stepIdx !== steps.length - 1 })}>
{stepIdx < currentStepIndex ? (
// Completed step
<>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="h-0.5 w-full bg-primary" />
</div>
<div
className="relative flex h-8 w-8 items-center justify-center rounded-full bg-primary"
>
<Check className="h-5 w-5 text-primary-foreground" aria-hidden="true" />
<span className="sr-only">{stepLabels[step]}</span>
</div>
</>
) : stepIdx === currentStepIndex ? (
// Current step
<>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="h-0.5 w-full bg-border" />
</div>
<div
className="relative flex h-8 w-8 items-center justify-center rounded-full border-2 border-primary bg-background"
aria-current="step"
>
<span className="h-2.5 w-2.5 rounded-full bg-primary" aria-hidden="true" />
<span className="sr-only">{stepLabels[step]}</span>
</div>
</>
) : (
// Upcoming step
<>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="h-0.5 w-full bg-border" />
</div>
<div
className="group relative flex h-8 w-8 items-center justify-center rounded-full border-2 border-border bg-background"
>
<span className="h-2.5 w-2.5 rounded-full bg-transparent" aria-hidden="true" />
<span className="sr-only">{stepLabels[step]}</span>
</div>
</>
)}
<p className="absolute -bottom-6 w-max text-center text-xs font-medium text-muted-foreground sm:text-sm">
{stepLabels[step]}
</p>
</li>
))}
</ol>
</nav>
);
}

View file

@ -0,0 +1,124 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { generateContentPlan } from '@/ai/flows/generate-content-plan';
import { generateContent } from '@/ai/flows/generate-content';
import { Stepper } from './components/stepper';
import { BriefForm } from './components/brief-form';
import { PlanEditor } from './components/plan-editor';
import { ContentEditor } from './components/content-editor';
import { useToast } from '@/hooks/use-toast';
import { Loader2 } from 'lucide-react';
import type { BriefData } from './components/brief-form';
type Step = 'brief' | 'plan' | 'content';
export default function CreatePage() {
const [step, setStep] = useState<Step>('brief');
const [brief, setBrief] = useState<string>('');
const [plan, setPlan] = useState<string>('');
const [content, setContent] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const { toast } = useToast();
const handleBriefSubmit = async (data: BriefData) => {
setIsLoading(true);
const briefText = `Project Title: ${data.title}\nContent Goal: ${data.goal}\nTarget Audience: ${data.audience}\nKey Talking Points: ${data.points}\nTone of Voice: ${data.tone}\nKeywords: ${data.keywords}`;
setBrief(briefText);
try {
const result = await generateContentPlan({ brief: briefText });
setPlan(result.contentPlan);
setStep('plan');
toast({
title: 'Content Plan Generated!',
description: 'Review and edit your new content plan.',
});
} catch (error) {
console.error(error);
toast({
variant: 'destructive',
title: 'Error Generating Plan',
description: 'Something went wrong. Please try again.',
});
} finally {
setIsLoading(false);
}
};
const handlePlanApprove = async () => {
setIsLoading(true);
try {
const result = await generateContent({ contentPlan: plan });
setContent(result.generatedContent);
setStep('content');
toast({
title: 'Content Generated!',
description: 'Your content is ready for final review.',
});
} catch (error) {
console.error(error);
toast({
variant: 'destructive',
title: 'Error Generating Content',
description: 'Something went wrong. Please try again.',
});
} finally {
setIsLoading(false);
}
};
const goBackToBrief = () => setStep('brief');
const goBackToPlan = () => setStep('plan');
const pageVariants = {
initial: { opacity: 0, x: 50 },
in: { opacity: 1, x: 0 },
out: { opacity: 0, x: -50 },
};
const pageTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.5,
};
return (
<div className="flex h-full flex-col">
<header className="flex h-16 shrink-0 items-center border-b px-6">
<div className="flex-1">
<h1 className="text-xl font-semibold">Create New Content</h1>
<p className="text-sm text-muted-foreground">Follow the steps to generate your content.</p>
</div>
</header>
<div className="flex-1 overflow-auto p-6">
<div className="mx-auto max-w-4xl">
<Stepper currentStep={step} />
<div className="relative mt-8">
{isLoading && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center rounded-lg bg-background/80 backdrop-blur-sm">
<Loader2 className="h-10 w-10 animate-spin text-primary" />
<p className="mt-4 text-muted-foreground">AI is thinking...</p>
</div>
)}
<AnimatePresence mode="wait">
<motion.div
key={step}
initial="initial"
animate="in"
exit="out"
variants={pageVariants}
transition={pageTransition}
>
{step === 'brief' && <BriefForm onSubmit={handleBriefSubmit} isLoading={isLoading} />}
{step === 'plan' && <PlanEditor plan={plan} setPlan={setPlan} onApprove={handlePlanApprove} onEditBrief={goBackToBrief} isLoading={isLoading} />}
{step === 'content' && <ContentEditor content={content} setContent={setContent} onEditPlan={goBackToPlan} />}
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,14 @@
import { AppSidebar } from '@/components/app/sidebar';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen w-full">
<AppSidebar />
<main className="flex-1">{children}</main>
</div>
);
}

View file

@ -0,0 +1,96 @@
import Link from 'next/link';
import Image from 'next/image';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import { PlusCircle, MoreVertical } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
const mockProjects = [
{
title: 'Q3 Marketing Campaign',
description: 'A series of blog posts about sustainable tech.',
status: 'In Progress',
imageUrl: 'https://placehold.co/600x400.png',
aiHint: 'technology marketing'
},
{
title: 'New Feature Launch',
description: 'Social media copy for the new analytics dashboard.',
status: 'Completed',
imageUrl: 'https://placehold.co/600x400.png',
aiHint: 'social media'
},
{
title: 'Website SEO Overhaul',
description: 'Generating SEO-optimized articles for key pages.',
status: 'Planning',
imageUrl: 'https://placehold.co/600x400.png',
aiHint: 'seo writing'
},
];
export default function DashboardPage() {
return (
<div className="flex h-full flex-col bg-background">
<header className="flex h-16 shrink-0 items-center border-b px-6">
<div className="flex-1">
<h1 className="text-xl font-semibold">Dashboard</h1>
<p className="text-sm text-muted-foreground">Welcome back! Here&apos;s your content overview.</p>
</div>
<div className="flex items-center gap-4">
<Button asChild>
<Link href="/dashboard/create">
<PlusCircle className="mr-2 h-4 w-4" />
Create New Content
</Link>
</Button>
</div>
</header>
<div className="flex-1 overflow-auto p-6">
<h2 className="mb-4 text-lg font-semibold tracking-tight">Recent Projects</h2>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{mockProjects.map((project) => (
<Card key={project.title} className="flex flex-col">
<CardHeader>
<Image
src={project.imageUrl}
alt={project.title}
width={600}
height={400}
className="mb-4 aspect-[3/2] w-full rounded-lg object-cover"
data-ai-hint={project.aiHint}
/>
<div className="flex items-start justify-between">
<CardTitle className="text-lg">{project.title}</CardTitle>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<CardDescription>{project.description}</CardDescription>
</CardHeader>
<CardContent className="flex-grow"></CardContent>
<CardFooter>
<Badge variant={project.status === 'Completed' ? 'secondary' : 'default'}>{project.status}</Badge>
</CardFooter>
</Card>
))}
</div>
</div>
</div>
);
}

View file

@ -8,73 +8,73 @@ body {
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--background: 220 13% 95%;
--foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--popover-foreground: 224 71.4% 4.1%;
--primary: 231 48% 48%;
--primary-foreground: 0 0% 100%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: 283 84% 65%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 231 48% 48%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-background: 0 0% 100%;
--sidebar-foreground: 224 71.4% 4.1%;
--sidebar-primary: 231 48% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 14.3% 95.9%;
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-ring: 231 48% 48%;
}
.dark {
--background: 0 0% 3.9%;
--background: 222 47% 11%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card: 222 47% 11%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover: 222 47% 11%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--primary: 231 48% 58%;
--primary-foreground: 0 0% 100%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 283 84% 70%;
--accent-foreground: 0 0% 100%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 231 48% 58%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-background: 222 47% 11%;
--sidebar-foreground: 0 0% 98%;
--sidebar-primary: 231 48% 58%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 0 0% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 231 48% 58%;
}
}

View file

@ -1,9 +1,10 @@
import type {Metadata} from 'next';
import './globals.css';
import { Toaster } from "@/components/ui/toaster"
export const metadata: Metadata = {
title: 'Firebase Studio App',
description: 'Generated by Firebase Studio',
title: 'ContentFlow',
description: 'Generate content with AI',
};
export default function RootLayout({
@ -12,13 +13,16 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></link>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Source+Code+Pro&display=swap" rel="stylesheet" />
</head>
<body className="font-body antialiased">{children}</body>
<body className="font-body antialiased">
{children}
<Toaster />
</body>
</html>
);
}

View file

@ -1,3 +1,51 @@
export default function Home() {
return <></>;
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Logo } from '@/components/logo';
import { Github } from 'lucide-react';
export default function LoginPage() {
return (
<div className="flex min-h-screen w-full items-center justify-center bg-background p-4">
<Card className="mx-auto w-full max-w-sm">
<CardHeader className="text-center">
<Logo className="mx-auto mb-4 h-8" />
<CardTitle className="text-2xl font-bold tracking-tight">Welcome to ContentFlow</CardTitle>
<CardDescription>Enter your credentials to access your account</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="m@example.com" required />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link href="#" className="ml-auto inline-block text-sm underline" prefetch={false}>
Forgot your password?
</Link>
</div>
<Input id="password" type="password" required />
</div>
<Button type="submit" className="w-full" asChild>
<Link href="/dashboard">Login</Link>
</Button>
<Button variant="outline" className="w-full">
<Github className="mr-2 h-4 w-4" />
Login with Google
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{' '}
<Link href="#" className="underline" prefetch={false}>
Sign up
</Link>
</div>
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,92 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Home, PlusCircle, Settings, User, LogOut, BotMessageSquare } from 'lucide-react';
import { Logo } from '@/components/logo';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';
const navItems = [
{ href: '/dashboard', icon: Home, label: 'Dashboard' },
{ href: '/dashboard/create', icon: PlusCircle, label: 'Create New' },
];
export function AppSidebar() {
const pathname = usePathname();
return (
<aside className="hidden h-screen w-64 flex-col border-r bg-card md:flex">
<div className="flex h-16 items-center border-b px-6">
<Link href="/dashboard" className="flex items-center gap-2 font-semibold" prefetch={false}>
<Logo className="h-6" />
</Link>
</div>
<nav className="flex-1 space-y-1 p-4">
{navItems.map((item) => (
<Link
key={item.label}
href={item.href}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary',
pathname === item.href && 'bg-muted text-primary'
)}
prefetch={false}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
))}
</nav>
<div className="mt-auto p-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex w-full items-center justify-start gap-2">
<Avatar className="h-8 w-8">
<AvatarImage src="https://placehold.co/100x100.png" alt="@user" />
<AvatarFallback>U</AvatarFallback>
</Avatar>
<div className="flex flex-col items-start">
<span className="text-sm font-medium">User</span>
<span className="text-xs text-muted-foreground">user@example.com</span>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">User</p>
<p className="text-xs leading-none text-muted-foreground">user@example.com</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/" prefetch={false}>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</aside>
);
}

32
src/components/logo.tsx Normal file
View file

@ -0,0 +1,32 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export function Logo({ className, ...props }: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 64"
className={cn('text-primary', className)}
{...props}
>
<defs>
<linearGradient id="logo-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={{ stopColor: 'hsl(var(--primary))' }} />
<stop offset="100%" style={{ stopColor: 'hsl(var(--accent))' }} />
</linearGradient>
</defs>
<text
x="50%"
y="50%"
dominantBaseline="middle"
textAnchor="middle"
fontSize="52"
fontWeight="bold"
fill="url(#logo-gradient)"
className="font-headline"
>
ContentFlow
</text>
</svg>
);
}

View file

@ -12,7 +12,7 @@ export default {
fontFamily: {
body: ['Inter', 'sans-serif'],
headline: ['Inter', 'sans-serif'],
code: ['monospace'],
code: ['"Source Code Pro"', 'monospace'],
},
colors: {
background: 'hsl(var(--background))',