diff --git a/Dockerfile.txt b/Dockerfile.txt new file mode 100644 index 0000000..6bb5c71 --- /dev/null +++ b/Dockerfile.txt @@ -0,0 +1,43 @@ +# Stage 1: Build the React Application +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package.json to install dependencies first (better caching) +COPY package.json ./ + +# Install all dependencies (including devDependencies for the build process) +RUN npm install + +# Copy the rest of the application source code +COPY . . + +# Build the frontend assets (Vite will output to /app/dist) +RUN npm run build + +# Stage 2: Setup the Production Server (Node.js) +FROM node:20-alpine + +WORKDIR /app + +# Copy package.json again for production dependencies +COPY package.json ./ + +# Install ONLY production dependencies (skips devDependencies like Vite/Typescript) +RUN npm install --omit=dev + +# Copy the built frontend assets from the 'builder' stage +COPY --from=builder /app/dist ./dist + +# Copy the server entry point +COPY server.js ./ + +# Set environment variables (defaults) +ENV NODE_ENV=production +ENV PORT=3000 + +# Expose the port the app runs on +EXPOSE 3000 + +# Start the Node.js server +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index d366559..e785dd8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ -# EmailManager -Email manager UniSa +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1PJrPIeFdvYwt0ImdYe7PMH6YCDGTYQxH + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/components/RichTextEditor.tsx b/components/RichTextEditor.tsx new file mode 100644 index 0000000..df9a704 --- /dev/null +++ b/components/RichTextEditor.tsx @@ -0,0 +1,206 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { + Bold, Italic, Underline, List, ListOrdered, Link, + Code, Type, Eraser, AlignLeft, AlignCenter, AlignRight, Braces +} from 'lucide-react'; + +interface Props { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; +} + +const RichTextEditor: React.FC = ({ value, onChange, placeholder, className = '' }) => { + const [isSourceMode, setIsSourceMode] = useState(false); + const contentRef = useRef(null); + const selectionRef = useRef(null); + + // Sync value to contentEditable div + useEffect(() => { + if (contentRef.current && !isSourceMode) { + if (document.activeElement !== contentRef.current) { + if (contentRef.current.innerHTML !== value) { + contentRef.current.innerHTML = value; + } + } + } + }, [value, isSourceMode]); + + const saveSelection = () => { + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0) { + const range = sel.getRangeAt(0); + // Ensure the selection is actually inside our editor + if (contentRef.current && contentRef.current.contains(range.commonAncestorContainer)) { + selectionRef.current = range; + } + } + }; + + const restoreSelection = () => { + if (contentRef.current) { + contentRef.current.focus(); + if (selectionRef.current) { + const sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + sel.addRange(selectionRef.current); + } + } else { + // Fallback: move cursor to the end if no selection saved + const range = document.createRange(); + range.selectNodeContents(contentRef.current); + range.collapse(false); + const sel = window.getSelection(); + sel?.removeAllRanges(); + sel?.addRange(range); + } + } + }; + + const execCommand = (command: string, value: string | undefined = undefined) => { + restoreSelection(); + document.execCommand(command, false, value); + handleInput(); + }; + + const handleInput = () => { + if (contentRef.current) { + onChange(contentRef.current.innerHTML); + saveSelection(); + } + }; + + const insertVariable = (e: React.MouseEvent) => { + e.preventDefault(); // Prevent default button behavior + e.stopPropagation(); + + const varName = prompt("Enter variable name (without brackets):", "variable_name"); + + if (varName) { + if (isSourceMode) { + alert("Switch to Visual mode to use the inserter, or type {{name}} manually."); + } else { + restoreSelection(); + const text = `{{${varName}}}`; + + // Try standard command first, fallback to range manipulation + const success = document.execCommand('insertText', false, text); + + if (!success) { + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0) { + const range = sel.getRangeAt(0); + range.deleteContents(); + const textNode = document.createTextNode(text); + range.insertNode(textNode); + // Move cursor after inserted text + range.setStartAfter(textNode); + range.setEndAfter(textNode); + sel.removeAllRanges(); + sel.addRange(range); + } + } + + handleInput(); + } + } + }; + + const ToolbarButton = ({ + icon: Icon, + command, + arg, + active = false, + title + }: { icon: any, command?: string, arg?: string, active?: boolean, title: string }) => ( + + ); + + return ( +
+ {/* Toolbar */} +
+
+ +
+ + {!isSourceMode && ( + <> + + + +
+ + + +
+ + +
+ + +
+ + + + )} +
+ + {/* Editor Area */} +
+ {isSourceMode ? ( +