From 3d9aa4e68981b249c92b7aa19192458d2b169d90 Mon Sep 17 00:00:00 2001 From: joaquintous <153678331+joaquintous@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:28:14 +0100 Subject: [PATCH 1/6] Add files via upload --- next.config.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 next.config.ts diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 000000000..a084bd938 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,34 @@ +import type {NextConfig} from 'next'; + +const nextConfig: NextConfig = { + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'placehold.co', + port: '', + pathname: '/**', + }, + { + protocol: 'https', + hostname: 'images.unsplash.com', + port: '', + pathname: '/**', + }, + { + protocol: 'https', + hostname: 'picsum.photos', + port: '', + pathname: '/**', + }, + ], + }, +}; + +export default nextConfig; From 701839bac29c480fbad0826264e78565c40af5d8 Mon Sep 17 00:00:00 2001 From: joaquintous <153678331+joaquintous@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:42:28 +0100 Subject: [PATCH 2/6] Add files via upload --- src/ai/dev.ts | 6 + .../analyze-photo-for-content-suggestions.ts | 54 ++ src/ai/flows/generate-content-from-comment.ts | 77 ++ src/ai/flows/improve-existing-post.ts | 77 ++ src/ai/genkit.ts | 7 + src/app/admin/layout.tsx | 5 + src/app/admin/page.tsx | 59 ++ src/app/favicon.ico | Bin 0 -> 15086 bytes src/app/globals.css | 88 ++ src/app/layout.tsx | 26 + src/app/page.tsx | 173 ++++ src/components/FirebaseErrorListener.tsx | 63 ++ src/components/content-generator.tsx | 440 ++++++++++ src/components/content-preview.tsx | 293 +++++++ src/components/history-dialog.tsx | 160 ++++ src/components/icons.tsx | 25 + src/components/ui/accordion.tsx | 58 ++ src/components/ui/alert-dialog.tsx | 141 ++++ src/components/ui/alert.tsx | 59 ++ src/components/ui/avatar.tsx | 50 ++ src/components/ui/badge.tsx | 36 + src/components/ui/button.tsx | 56 ++ src/components/ui/calendar.tsx | 70 ++ src/components/ui/card.tsx | 79 ++ src/components/ui/carousel.tsx | 262 ++++++ src/components/ui/chart.tsx | 365 +++++++++ src/components/ui/checkbox.tsx | 30 + src/components/ui/collapsible.tsx | 11 + src/components/ui/dialog.tsx | 122 +++ src/components/ui/dropdown-menu.tsx | 200 +++++ src/components/ui/form.tsx | 178 ++++ src/components/ui/input.tsx | 22 + src/components/ui/label.tsx | 26 + src/components/ui/menubar.tsx | 256 ++++++ src/components/ui/popover.tsx | 31 + src/components/ui/progress.tsx | 28 + src/components/ui/radio-group.tsx | 44 + src/components/ui/scroll-area.tsx | 48 ++ src/components/ui/select.tsx | 160 ++++ src/components/ui/separator.tsx | 31 + src/components/ui/sheet.tsx | 140 ++++ src/components/ui/sidebar.tsx | 763 ++++++++++++++++++ src/components/ui/skeleton.tsx | 15 + src/components/ui/slider.tsx | 28 + src/components/ui/switch.tsx | 29 + src/components/ui/table.tsx | 117 +++ src/components/ui/tabs.tsx | 55 ++ src/components/ui/textarea.tsx | 21 + src/components/ui/toast.tsx | 129 +++ src/components/ui/toaster.tsx | 35 + src/components/ui/tooltip.tsx | 30 + src/firebase/auth/use-user.tsx | 64 ++ src/firebase/client-provider.tsx | 195 +++++ src/firebase/config.ts | 29 + src/firebase/error-emitter.ts | 22 + src/firebase/errors.ts | 24 + src/firebase/firestore/use-collection.tsx | 88 ++ src/firebase/firestore/use-doc.tsx | 52 ++ src/firebase/index.ts | 44 + src/firebase/provider.tsx | 59 ++ src/hooks/use-memo-firebase.ts | 51 ++ src/hooks/use-mobile.tsx | 19 + src/hooks/use-toast.ts | 194 +++++ src/lib/actions.ts | 324 ++++++++ src/lib/connections.ts | 55 ++ src/lib/history.ts | 32 + src/lib/placeholder-images.json | 3 + src/lib/placeholder-images.ts | 10 + src/lib/types.ts | 50 ++ src/lib/utils.ts | 6 + 70 files changed, 6599 insertions(+) create mode 100644 src/ai/dev.ts create mode 100644 src/ai/flows/analyze-photo-for-content-suggestions.ts create mode 100644 src/ai/flows/generate-content-from-comment.ts create mode 100644 src/ai/flows/improve-existing-post.ts create mode 100644 src/ai/genkit.ts create mode 100644 src/app/admin/layout.tsx create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/favicon.ico create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/components/FirebaseErrorListener.tsx create mode 100644 src/components/content-generator.tsx create mode 100644 src/components/content-preview.tsx create mode 100644 src/components/history-dialog.tsx create mode 100644 src/components/icons.tsx create mode 100644 src/components/ui/accordion.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/calendar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/carousel.tsx create mode 100644 src/components/ui/chart.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/collapsible.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/menubar.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/components/ui/radio-group.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/sidebar.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/firebase/auth/use-user.tsx create mode 100644 src/firebase/client-provider.tsx create mode 100644 src/firebase/config.ts create mode 100644 src/firebase/error-emitter.ts create mode 100644 src/firebase/errors.ts create mode 100644 src/firebase/firestore/use-collection.tsx create mode 100644 src/firebase/firestore/use-doc.tsx create mode 100644 src/firebase/index.ts create mode 100644 src/firebase/provider.tsx create mode 100644 src/hooks/use-memo-firebase.ts create mode 100644 src/hooks/use-mobile.tsx create mode 100644 src/hooks/use-toast.ts create mode 100644 src/lib/actions.ts create mode 100644 src/lib/connections.ts create mode 100644 src/lib/history.ts create mode 100644 src/lib/placeholder-images.json create mode 100644 src/lib/placeholder-images.ts create mode 100644 src/lib/types.ts create mode 100644 src/lib/utils.ts diff --git a/src/ai/dev.ts b/src/ai/dev.ts new file mode 100644 index 000000000..57ddbe121 --- /dev/null +++ b/src/ai/dev.ts @@ -0,0 +1,6 @@ +import { config } from 'dotenv'; +config(); + +import '@/ai/flows/analyze-photo-for-content-suggestions.ts'; +import '@/ai/flows/improve-existing-post.ts'; +import '@/ai/flows/generate-content-from-comment.ts'; \ No newline at end of file diff --git a/src/ai/flows/analyze-photo-for-content-suggestions.ts b/src/ai/flows/analyze-photo-for-content-suggestions.ts new file mode 100644 index 000000000..6eb5ded88 --- /dev/null +++ b/src/ai/flows/analyze-photo-for-content-suggestions.ts @@ -0,0 +1,54 @@ +'use server'; +/** + * @fileOverview Analyzes a photo to suggest relevant topics and content ideas for a WordPress post. + * + * - analyzePhotoForContentSuggestions - A function that handles the photo analysis and content suggestion process. + * - AnalyzePhotoForContentSuggestionsInput - The input type for the analyzePhotoForContentSuggestions function. + * - AnalyzePhotoForContentSuggestionsOutput - The return type for the analyzePhotoForContentSuggestions function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const AnalyzePhotoForContentSuggestionsInputSchema = z.object({ + photoDataUri: z + .string() + .describe( + "A photo, as a data URI that must include a MIME type and use Base64 encoding. Expected format: 'data:;base64,'." + ), +}); +export type AnalyzePhotoForContentSuggestionsInput = z.infer; + +const AnalyzePhotoForContentSuggestionsOutputSchema = z.object({ + suggestedTopics: z.array(z.string()).describe('A list of suggested topics for a WordPress post based on the photo.'), + contentIdeas: z.array(z.string()).describe('A list of content ideas for a WordPress post based on the photo.'), +}); +export type AnalyzePhotoForContentSuggestionsOutput = z.infer; + +export async function analyzePhotoForContentSuggestions(input: AnalyzePhotoForContentSuggestionsInput): Promise { + return analyzePhotoForContentSuggestionsFlow(input); +} + +const prompt = ai.definePrompt({ + name: 'analyzePhotoForContentSuggestionsPrompt', + input: {schema: AnalyzePhotoForContentSuggestionsInputSchema}, + output: {schema: AnalyzePhotoForContentSuggestionsOutputSchema}, + prompt: `You are an expert content strategist for WordPress. You will analyze the photo provided and suggest relevant topics and content ideas for a WordPress post. + + Photo: {{media url=photoDataUri}} + + Please provide the suggested topics and content ideas in a JSON format. + `, +}); + +const analyzePhotoForContentSuggestionsFlow = ai.defineFlow( + { + name: 'analyzePhotoForContentSuggestionsFlow', + inputSchema: AnalyzePhotoForContentSuggestionsInputSchema, + outputSchema: AnalyzePhotoForContentSuggestionsOutputSchema, + }, + async input => { + const {output} = await prompt(input); + return output!; + } +); diff --git a/src/ai/flows/generate-content-from-comment.ts b/src/ai/flows/generate-content-from-comment.ts new file mode 100644 index 000000000..ec80cfa23 --- /dev/null +++ b/src/ai/flows/generate-content-from-comment.ts @@ -0,0 +1,77 @@ +// src/ai/flows/generate-content-from-comment.ts +'use server'; +/** + * @fileOverview Generates WordPress content (title, body, tags) from a user comment. + * + * - generateContentFromComment - A function that generates WordPress content based on a comment. + * - GenerateContentFromCommentInput - The input type for the generateContentFromComment function. + * - GenerateContentFromCommentOutput - The return type for the generateContentFromComment function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const GenerateContentFromCommentInputSchema = z.object({ + comment: z.string().describe('The comment to generate content from.'), +}); +export type GenerateContentFromCommentInput = z.infer; + +const GenerateContentFromCommentOutputSchema = z.object({ + title: z.string().describe('The title of the WordPress post.'), + body: z.string().describe('The body of the WordPress post.'), + tags: z.array(z.string()).describe('Suggested tags for the WordPress post.'), +}); +export type GenerateContentFromCommentOutput = z.infer; + +export async function generateContentFromComment(input: GenerateContentFromCommentInput): Promise { + return generateContentFromCommentFlow(input); +} + +const prompt = ai.definePrompt({ + name: 'generateContentFromCommentPrompt', + input: {schema: GenerateContentFromCommentInputSchema}, + output: {schema: GenerateContentFromCommentOutputSchema}, + prompt: `You are an AI assistant that generates WordPress content based on user comments. + + Based on the following comment, generate a title, body, and suggested tags for a WordPress post. + + Comment: {{{comment}}} + + The title should be concise and engaging. + The body should be informative and well-written. + The tags should be relevant and helpful for SEO. + + Ensure that the title, body and tags are appropriate and professional. + `,config: { + safetySettings: [ + { + category: 'HARM_CATEGORY_HATE_SPEECH', + threshold: 'BLOCK_ONLY_HIGH', + }, + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_HARASSMENT', + threshold: 'BLOCK_MEDIUM_AND_ABOVE', + }, + { + category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + ], + }, +}); + +const generateContentFromCommentFlow = ai.defineFlow( + { + name: 'generateContentFromCommentFlow', + inputSchema: GenerateContentFromCommentInputSchema, + outputSchema: GenerateContentFromCommentOutputSchema, + }, + async input => { + const {output} = await prompt(input); + return output!; + } +); diff --git a/src/ai/flows/improve-existing-post.ts b/src/ai/flows/improve-existing-post.ts new file mode 100644 index 000000000..88458c85f --- /dev/null +++ b/src/ai/flows/improve-existing-post.ts @@ -0,0 +1,77 @@ +'use server'; + +/** + * @fileOverview Rewrites an existing blog post to match a desired style, generating a new title and tags. + * + * - improveExistingPost - A function that handles the rewriting process. + * - ImproveExistingPostInput - The input type for the improveExistingPost function. + * - ImproveExistingPostOutput - The return type for the improveExistingPost function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const ImproveExistingPostInputSchema = z.object({ + existingPost: z.string().describe('The existing blog post content.'), + desiredStyle: z.string().describe('The desired style for the blog post.'), +}); +export type ImproveExistingPostInput = z.infer; + +const ImproveExistingPostOutputSchema = z.object({ + title: z.string().describe('The new, improved title for the blog post.'), + body: z.string().describe('The rewritten blog post content.'), + tags: z.array(z.string()).describe('A list of relevant tags for the rewritten post.'), +}); +export type ImproveExistingPostOutput = z.infer; + +export async function improveExistingPost(input: ImproveExistingPostInput): Promise { + return improveExistingPostFlow(input); +} + +const prompt = ai.definePrompt({ + name: 'improveExistingPostPrompt', + input: {schema: ImproveExistingPostInputSchema}, + output: {schema: ImproveExistingPostOutputSchema}, + prompt: `You are an expert blog post writer. You will rewrite the existing blog post to match the desired style. +You must also generate a new, catchy title for the post and a list of relevant tags. + +Existing Blog Post: +{{{existingPost}}} + +Desired Style: +{{{desiredStyle}}} + +Respond with the rewritten post body, a new title, and new tags in the specified JSON format.`, + config: { + safetySettings: [ + { + category: 'HARM_CATEGORY_HATE_SPEECH', + threshold: 'BLOCK_ONLY_HIGH', + }, + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_HARASSMENT', + threshold: 'BLOCK_MEDIUM_AND_ABOVE', + }, + { + category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + threshold: 'BLOCK_LOW_AND_ABOVE', + }, + ], + }, +}); + +const improveExistingPostFlow = ai.defineFlow( + { + name: 'improveExistingPostFlow', + inputSchema: ImproveExistingPostInputSchema, + outputSchema: ImproveExistingPostOutputSchema, + }, + async input => { + const {output} = await prompt(input); + return output!; + } +); diff --git a/src/ai/genkit.ts b/src/ai/genkit.ts new file mode 100644 index 000000000..8811f2c56 --- /dev/null +++ b/src/ai/genkit.ts @@ -0,0 +1,7 @@ +import {genkit} from 'genkit'; +import {googleAI} from '@genkit-ai/google-genai'; + +export const ai = genkit({ + plugins: [googleAI()], + model: 'googleai/gemini-2.5-flash', +}); diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 000000000..43a570c3b --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,5 @@ +import { ReactNode } from 'react'; + +export default function AdminLayout({ children }: { children: ReactNode }) { + return
{children}
; +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 000000000..4ff19c5e3 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { ArrowLeft, Shield, UserCog } from 'lucide-react'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function AdminPage() { + return ( +
+
+
+ +

User Management

+
+ +
+ + + + + + Admin Panel Functionality + + + +

+ This page is a placeholder for user management functionality. Listing all users from the client is disabled by default for security reasons. +

+
+

How to Implement This Securely

+

+ To build a secure admin panel, you should use Firebase's server-side features: +

+
    +
  1. + Custom Claims: Assign an `admin: true` custom claim to trusted users using the Firebase Admin SDK (typically in a Cloud Function). +
  2. +
  3. + Security Rules: Update your Firestore security rules to only allow users with the `admin: true` claim to list all documents in the `/users` collection. +
  4. +
  5. + Backend Logic: For advanced user management (like deleting users or changing roles), create secure Cloud Functions that can be called from this admin panel. +
  6. +
+
+

+ This ensures that only authorized administrators can view and manage user data, protecting the privacy of your users. +

+
+
+
+ ); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9621e46d3413d55917e344b844a6605e8d9359ac GIT binary patch literal 15086 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 N(HJzA7tjZd{r?^>IY|Hj literal 0 HcmV?d00001 diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 000000000..8431531ab --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,88 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Arial, Helvetica, sans-serif; +} + +@layer base { + :root { + --background: 220 17% 95%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 231 48% 48%; + --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: 174 100% 29%; + --accent-foreground: 0 0% 98%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --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-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 231 48% 48%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 174 100% 29%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 231 48% 48%; + --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-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%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 000000000..deffdfa02 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from 'next'; +import './globals.css'; +import { Toaster } from '@/components/ui/toaster'; +import { FirebaseClientProvider } from '@/firebase'; + +export const metadata: Metadata = { + title: 'WP Content PRO', + description: 'Generate WordPress content with AI', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 000000000..27077ab01 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,173 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import type { GeneratedContent } from '@/lib/types'; +import { ContentGenerator } from '@/components/content-generator'; +import { ContentPreview } from '@/components/content-preview'; +import { useUser, useAuth, useLanguage } from '@/firebase'; +import { GoogleAuthProvider, signInWithRedirect, getRedirectResult } from 'firebase/auth'; +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 { History, LogOut, Languages, Shield, Loader2 } from 'lucide-react'; +import { HistoryDialog } from '@/components/history-dialog'; +import { Icons } from '@/components/icons'; +import Link from 'next/link'; + +export default function Home() { + const [generatedContent, setGeneratedContent] = useState(null); + const [contentSource, setContentSource] = useState<'comment' | 'photo' | 'improve' | null>(null); + const [photoPreview, setPhotoPreview] = useState(null); + const [historyOpen, setHistoryOpen] = useState(false); + + const { user, loading: userLoading, signOut } = useUser(); + const auth = useAuth(); + const { t, setLanguage, language } = useLanguage(); + + const handleLogin = async () => { + if (!auth) return; + const provider = new GoogleAuthProvider(); + try { + await signInWithRedirect(auth, provider); + } catch (error) { + console.error("Error during sign-in:", error); + } + }; + + useEffect(() => { + if (auth) { + getRedirectResult(auth).catch(error => { + if (error.code !== 'auth/cancelled-popup-request' && error.code !== 'auth/popup-closed-by-user') { + console.error("Error getting redirect result:", error); + } + }); + } + }, [auth]); + + + const handleContentGenerated = (content: GeneratedContent, source: 'comment' | 'photo' | 'improve', photoUrl?: string) => { + setGeneratedContent(content); + setContentSource(source); + if (photoUrl) { + setPhotoPreview(photoUrl); + } else { + setPhotoPreview(null); + } + }; + + const handleReset = () => { + setGeneratedContent(null); + setContentSource(null); + setPhotoPreview(null); + }; + + const getInitials = (name?: string | null) => { + if (!name) return 'U'; + const names = name.split(' '); + if (names.length > 1) { + return `${names[0][0]}${names[names.length - 1][0]}`; + } + return names[0][0]; + }; + + return ( +
+
+
+
+ + {t('appTitle')} +
+
+ {userLoading ? ( +
+ ) : user ? ( +
+ + + + + + + +
+

{user.displayName}

+

{user.email}

+
+
+ + + {t('adminPanel')} + + + + + {t('logout')} + +
+
+
+ ) : ( + + )} + + + + + + {t('language')} + setLanguage('en')} disabled={language === 'en'}>English + setLanguage('es')} disabled={language === 'es'}>Español + + +
+
+ +
+
+ {userLoading ? ( +
+ +

Loading session...

+
+ ) : !user ? ( +
+

{t('welcome')}

+

+ {t('loginToContinue')} +

+ +
+ ) : generatedContent ? ( + + ) : ( + + )} +
+
+ +
+
+

{t('footerText')}

+
+
+ +
+ ); +} diff --git a/src/components/FirebaseErrorListener.tsx b/src/components/FirebaseErrorListener.tsx new file mode 100644 index 000000000..8e9a440dd --- /dev/null +++ b/src/components/FirebaseErrorListener.tsx @@ -0,0 +1,63 @@ +'use client'; + +import React, { useEffect } from 'react'; +import { errorEmitter } from '@/firebase/error-emitter'; +import { FirestorePermissionError } from '@/firebase/errors'; +import { useToast } from '@/hooks/use-toast'; + +// This component is a client-side component that listens for Firebase permission errors +// and displays them in a toast notification. This is useful for debugging security rules. +// It should be placed in your root layout or a high-level provider. +export function FirebaseErrorListener() { + const { toast } = useToast(); + + useEffect(() => { + const handlePermissionError = (error: any) => { + // Use property checking instead of instanceof for robustness in dev environments + if (error && error.name === 'FirestorePermissionError' && error.context) { + const errorContext = { + message: error.message, + context: error.context, + }; + // Use JSON.stringify to ensure the object is fully logged in the console + console.error('Firestore Permission Error:', JSON.stringify(errorContext, null, 2)); + + // In a real app, you might want to log this to a service like Sentry or Bugsnag. + // For this starter, we'll show a toast in development. + if (process.env.NODE_ENV === 'development') { + toast({ + variant: 'destructive', + title: 'Firestore Permission Denied', + description: ( +
+                
+                  {JSON.stringify(errorContext, null, 2)}
+                
+              
+ ), + duration: 20000, + }); + } + } else { + // Log a generic error if the emitted object is not what we expect + console.error('An unknown permission error occurred:', error); + if (process.env.NODE_ENV === 'development') { + toast({ + variant: 'destructive', + title: 'Unknown Permission Error', + description: 'Check the browser console for more details.', + duration: 20000, + }); + } + } + }; + + errorEmitter.on('permission-error', handlePermissionError); + + return () => { + errorEmitter.off('permission-error', handlePermissionError); + }; + }, [toast]); + + return null; // This component doesn't render anything +} diff --git a/src/components/content-generator.tsx b/src/components/content-generator.tsx new file mode 100644 index 000000000..e8b1d7152 --- /dev/null +++ b/src/components/content-generator.tsx @@ -0,0 +1,440 @@ +'use client'; + +import { useState } from 'react'; +import { useForm, type SubmitHandler } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Loader2, FileImage, Sparkles, Search, KeyRound, Globe, BookUp } from 'lucide-react'; +import Image from 'next/image'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Textarea } from '@/components/ui/textarea'; +import { Input } from '@/components/ui/input'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { useToast } from '@/hooks/use-toast'; +import { generateFromCommentAction, generateFromPhotoAction, improvePostAction, searchWordPressPostsAction, getWordPressPostAction } from '@/lib/actions'; +import type { GeneratedContent, WpPost, WpConnection } from '@/lib/types'; +import { Separator } from './ui/separator'; +import { Label } from './ui/label'; +import { ScrollArea } from './ui/scroll-area'; +import { RadioGroup, RadioGroupItem } from './ui/radio-group'; +import { useUser, useFirestore, useCollection, useLanguage } from '@/firebase'; +import { addHistoryEvent } from '@/lib/history'; +import { addOrUpdateConnection } from '@/lib/connections'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; +import { useMemoFirebase } from '@/hooks/use-memo-firebase'; +import { collection, query, orderBy } from 'firebase/firestore'; + + +const commentSchema = z.object({ + comment: z.string().min(10, 'Please enter a comment of at least 10 characters.'), +}); + +const photoSchema = z.object({ + photo: z.any().refine((file) => file?.length == 1, 'Photo is required.'), +}); + +const improveSchema = z.object({ + existingPost: z.string().min(10, 'Please enter existing content of at least 10 characters.'), + desiredStyle: z.string().min(5, 'Please enter a desired style of at least 5 characters.'), +}); + +type ContentGeneratorProps = { + onContentGenerated: (content: GeneratedContent, source: 'comment' | 'photo' | 'improve', photoUrl?: string) => void; +}; + +export function ContentGenerator({ onContentGenerated }: ContentGeneratorProps) { + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState('comment'); + const [photoPreview, setPhotoPreview] = useState(null); + const { toast } = useToast(); + const { user, loading: userLoading } = useUser(); + const firestore = useFirestore(); + const { t } = useLanguage(); + + // State for WP Search + const [isSearching, setIsSearching] = useState(false); + const [wpUrl, setWpUrl] = useState(''); + const [wpUsername, setWpUsername] = useState(''); + const [wpAppPassword, setWpAppPassword] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [postIdToUpdate, setPostIdToUpdate] = useState(undefined); + const [postType, setPostType] = useState<'posts' | 'pages'>('posts'); + + // Fetch connections + const connectionsQuery = useMemoFirebase(() => { + if (userLoading || !user || !firestore) return null; + return query(collection(firestore, `users/${user.uid}/connections`), orderBy('lastUsed', 'desc')); + }, [user, firestore, userLoading]); + const { data: connections } = useCollection(connectionsQuery); + + const commentForm = useForm>({ + resolver: zodResolver(commentSchema), + defaultValues: { comment: '' }, + }); + + const photoForm = useForm>({ + resolver: zodResolver(photoSchema), + }); + + const improveForm = useForm>({ + resolver: zodResolver(improveSchema), + defaultValues: { existingPost: '', desiredStyle: '' }, + }); + + const handleCommentSubmit: SubmitHandler> = async (data) => { + setIsLoading(true); + try { + const result = await generateFromCommentAction(data.comment); + onContentGenerated(result, 'comment'); + } catch (error) { + toast({ + variant: 'destructive', + title: 'An error occurred', + description: error instanceof Error ? error.message : 'Please try again later.', + }); + } finally { + setIsLoading(false); + } + }; + + const handlePhotoSubmit: SubmitHandler> = async (data) => { + setIsLoading(true); + const file = data.photo[0]; + if (!file) { + setIsLoading(false); + return; + } + + const reader = new FileReader(); + reader.onload = async (e) => { + const dataUri = e.target?.result as string; + if (!dataUri) { + toast({ variant: 'destructive', title: 'Could not read file.' }); + setIsLoading(false); + return; + } + + try { + const result = await generateFromPhotoAction(dataUri); + onContentGenerated(result, 'photo', dataUri); + } catch (error) { + toast({ + variant: 'destructive', + title: 'An error occurred', + description: error instanceof Error ? error.message : 'Please try again later.', + }); + } finally { + setIsLoading(false); + } + }; + reader.onerror = () => { + toast({ variant: 'destructive', title: 'Error reading file.' }); + setIsLoading(false); + }; + reader.readAsDataURL(file); + }; + + const handleImproveSubmit: SubmitHandler> = async (data) => { + setIsLoading(true); + try { + const result = await improvePostAction(data.existingPost, data.desiredStyle); + onContentGenerated({ ...result, postId: postIdToUpdate, postType: postType }, 'improve'); + } catch (error) { + toast({ + variant: 'destructive', + title: 'An error occurred', + description: error instanceof Error ? error.message : 'Please try again later.', + }); + } finally { + setIsLoading(false); + } + }; + + const onPhotoChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if(file){ + const url = URL.createObjectURL(file); + setPhotoPreview(url); + } else { + setPhotoPreview(null); + } + } + + const handleSearch = async () => { + setIsSearching(true); + setSearchResults([]); + try { + const results = await searchWordPressPostsAction(searchQuery, wpUrl, wpUsername, wpAppPassword, postType); + setSearchResults(results); + + if (results.length > 0 && user && firestore) { + addOrUpdateConnection(firestore, user.uid, { wpUrl, wpUsername }); + addHistoryEvent(firestore, user.uid, { + type: 'connection', + details: { + wpUrl: wpUrl, + wpUsername: wpUsername, + } + }); + } + + if (results.length === 0) { + toast({ title: "No results found." }); + } + } catch(error) { + toast({ + variant: "destructive", + title: "Search failed", + description: error instanceof Error ? error.message : "Could not perform search.", + }); + } finally { + setIsSearching(false); + } + } + + const handleSelectPost = (post: WpPost) => { + improveForm.setValue('existingPost', post.content.rendered); + setPostIdToUpdate(post.id.toString()); + toast({ title: "Post loaded", description: `"${post.title.rendered}" is ready to be improved.` }); + } + + const handleSelectConnection = (connectionId: string) => { + const conn = connections?.find(c => c.id === connectionId); + if (conn) { + setWpUrl(conn.wpUrl); + setWpUsername(conn.wpUsername); + } + } + + return ( + + + + + Create Your Content + + + Choose your source and let AI craft a WordPress post for you. + + + + + + From Comment + From Photo + Improve Post + + +
+ + ( + + Your comment or idea + +