diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 0000000..04c197a --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine as development + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Expose port +EXPOSE 5173 + +# Start development server +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/client/Dockerfile.prod b/client/Dockerfile.prod new file mode 100644 index 0000000..d5b7014 --- /dev/null +++ b/client/Dockerfile.prod @@ -0,0 +1,25 @@ +# build react app +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package*.json ./ + +RUN npm ci + +COPY . . +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL +RUN npm run build + +# server the build files with nginx +FROM nginx:stable-alpine + +COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf + +COPY --from=build /app/dist /usr/share/nginx/html + + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/client/nginx/nginx.conf b/client/nginx/nginx.conf new file mode 100644 index 0000000..73b98a1 --- /dev/null +++ b/client/nginx/nginx.conf @@ -0,0 +1,9 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html =404; + } +} \ No newline at end of file diff --git a/client/src/pages/NoteDetails.tsx b/client/src/pages/NoteDetails.tsx deleted file mode 100644 index e3022a5..0000000 --- a/client/src/pages/NoteDetails.tsx +++ /dev/null @@ -1,565 +0,0 @@ -import React, { useState } from "react"; -import { - Box, - Typography, - Paper, - Stack, - Button, - Chip, - IconButton, - Menu, - MenuItem, - ListItemIcon, - ListItemText, - CircularProgress, - Alert, - Divider, - Grid, - Card, - CardContent, - Dialog, - DialogTitle, - DialogContent, - DialogContentText, - DialogActions, - Tooltip, - Avatar, -} from "@mui/material"; -import { - ArrowBack as ArrowBackIcon, - Edit as EditIcon, - Delete as DeleteIcon, - FileDownload as ExportIcon, - Share as ShareIcon, - MoreVert as MoreVertIcon, - BookmarkBorder as BookmarkIcon, - Bookmark as BookmarkedIcon, - SentimentVeryDissatisfied, - SentimentNeutral, - SentimentVerySatisfied, - CalendarToday as DateIcon, - Tag as TagIcon, - List as ListIcon, -} from "@mui/icons-material"; -import { useNavigate, useParams } from "react-router-dom"; -import { useNote, useDeleteNote } from "../hooks"; -import { noteToLegacy } from "../utils/dataTransforms"; -import { sentimentColors, tagColors } from "../theme/theme"; -import { saveAs } from "file-saver"; -import ReactMarkdown from "react-markdown"; - -const NoteDetails: React.FC = () => { - const navigate = useNavigate(); - const { id } = useParams<{ id: string }>(); - const deleteNoteMutation = useDeleteNote(); - - const [anchorEl, setAnchorEl] = useState(null); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [isBookmarked, setIsBookmarked] = useState(false); - - const { - data: note, - isLoading, - error, - } = useNote(id!, { - enabled: !!id, - }); - - const handleMenuOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleMenuClose = () => { - setAnchorEl(null); - }; - - const handleEdit = () => { - handleMenuClose(); - navigate(`/notes/${id}/edit`); - }; - - const handleDeleteClick = () => { - handleMenuClose(); - setDeleteDialogOpen(true); - }; - - const handleDeleteConfirm = () => { - if (!id) return; - - deleteNoteMutation.mutate(id, { - onSuccess: () => { - setDeleteDialogOpen(false); - navigate("/"); - }, - onError: (error) => { - console.error("Failed to delete note:", error); - setDeleteDialogOpen(false); - }, - }); - }; - - const handleExport = () => { - if (!note) return; - handleMenuClose(); - - const legacyNote = noteToLegacy(note); - const markdown = `# ${note.title} - -**Created:** ${new Date(note.createdAt).toLocaleDateString()} -**Last Updated:** ${new Date(note.updatedAt).toLocaleDateString()} -**Tags:** ${note.tags.join(", ")} -**Sentiment:** ${note.sentiment.label} (Score: ${note.sentiment.score}) - -## Summary -${note.summary} - -## Key Points -${note.keyPoints.map((point) => `- ${point}`).join("\n")} - -## Content -${note.content}`; - - const blob = new Blob([markdown], { type: "text/markdown" }); - saveAs(blob, `${note.title}.md`); - }; - - const handleShare = () => { - handleMenuClose(); - if (navigator.share) { - navigator.share({ - title: note?.title, - text: note?.summary, - url: window.location.href, - }); - } else { - // Fallback: copy to clipboard - navigator.clipboard.writeText(window.location.href); - } - }; - - const handleBookmark = () => { - setIsBookmarked(!isBookmarked); - // TODO: Implement bookmark functionality - }; - - const getSentimentIcon = (sentiment: { label: string; score: number }) => { - const iconProps = { - sx: { - fontSize: 20, - color: - sentimentColors[sentiment.label as keyof typeof sentimentColors] - ?.main || sentimentColors.neutral.main, - }, - }; - - switch (sentiment.label) { - case "positive": - return ; - case "negative": - return ; - case "neutral": - default: - return ; - } - }; - - const getTagColor = (index: number): string => { - return tagColors[index % tagColors.length]; - }; - - const formatDate = (date: Date | string) => { - const d = new Date(date); - return d.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - }; - - if (isLoading) { - return ( - - - - ); - } - - if (error) { - return ( - - - {error.message} - - - - ); - } - - if (!note) { - return ( - - - Note not found - - - - ); - } - - return ( - - {/* Header */} - - - - - - {note.title} - - - - - - Created: {formatDate(note.createdAt)} - - - - - - Updated: {formatDate(note.updatedAt)} - - - - - - - - - - {isBookmarked ? : } - - - - - - - - - - - {/* Main Content */} - - - {/* Summary */} - {note.summary && ( - - - - - S - - Summary - - - {note.summary} - - - - )} - - {/* Content */} - - - - C - - Content - - - {note.content} - - - - - - {/* Sidebar */} - - - {/* Metadata */} - - - - Metadata - - - {/* Sentiment */} - - - Sentiment - - - {getSentimentIcon(note.sentiment)} - - {note.sentiment.label} ({note.sentiment.score.toFixed(1)} - ) - - - - - - - {/* Tags */} - {note.tags.length > 0 && ( - - - - Tags - - - {note.tags.map((tag, index) => ( - - ))} - - - )} - - {note.tags.length > 0 && note.keyPoints.length > 0 && ( - - )} - - {/* Key Points */} - {note.keyPoints.length > 0 && ( - - - - Key Points - - - {note.keyPoints.map((point, index) => ( - - - {point} - - ))} - - - )} - - - - - - - - {/* Actions Menu */} - - - - - - Export as Markdown - - - - - - Share - - - - - - - Delete - - - - {/* Delete Confirmation Dialog */} - setDeleteDialogOpen(false)} - maxWidth="sm" - fullWidth - > - Delete Note - - - Are you sure you want to delete "{note.title}"? This action cannot - be undone. - - - - - - - - - ); -}; - -export default NoteDetails; \ No newline at end of file diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index c9ccbd4..e10d52d 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -16,12 +16,14 @@ "jsx": "react-jsx", /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "noImplicitAny": false, + "noImplicitReturns": false }, "include": ["src"] } diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json index 9728af2..2fe95cc 100644 --- a/client/tsconfig.node.json +++ b/client/tsconfig.node.json @@ -14,12 +14,14 @@ "noEmit": true, /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "noImplicitAny": false, + "noImplicitReturns": false }, "include": ["vite.config.ts"] } diff --git a/compose.yml b/compose.yml index d29e475..d29413d 100644 --- a/compose.yml +++ b/compose.yml @@ -1,4 +1,36 @@ services: + client: + build: + context: ./client + dockerfile: Dockerfile + ports: + - "5173:5173" + volumes: + - ./client:/app + - /app/node_modules + environment: + - VITE_API_URL=http://localhost:8007 + depends_on: + - server + server: + build: + context: ./server + dockerfile: Dockerfile + ports: + - "8007:8007" + volumes: + - ./server:/app + - /app/node_modules + environment: + - NODE_ENV=development + - PORT=8007 + - MONGODB_URI=mongodb://nm-mongo:27017/note_manager + - CORS_ORIGIN=http://localhost:5173 + - AI_SERVICE_URL=ai-service-url + - AI_API_KEY=ai-api-key + depends_on: + - nm-mongo + nm-mongo: healthcheck: test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet @@ -8,4 +40,28 @@ services: - "27017:27017" volumes: - ./.data/mongo/data:/data/db - - ./.data/mongo/config:/data/configdb \ No newline at end of file + - ./.data/mongo/config:/data/configdb + + + client-nginx: + build: + context: ./client + dockerfile: Dockerfile.prod + args: + - VITE_API_URL=http://localhost:8007 + ports: + - "80:80" + depends_on: + - server-prod + server-prod: + build: + context: ./server + dockerfile: Dockerfile.prod + ports: + - "8007:8007" + environment: + - PORT=8007 + - MONGODB_URI=${MONGODB_URI} + - AI_SERVICE_URL=ai-service-url + - AI_API_KEY=ai-api-key + \ No newline at end of file diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..2c032e3 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,20 @@ +# Development stage +FROM node:20-alpine as development + +WORKDIR /app + + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Expose port +EXPOSE 8000 + +# Start development server +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/server/Dockerfile.prod b/server/Dockerfile.prod new file mode 100644 index 0000000..7ce1954 --- /dev/null +++ b/server/Dockerfile.prod @@ -0,0 +1,35 @@ +# Production build stage +FROM node:20-alpine as build + +WORKDIR /app + + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM node:20-alpine as production + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install only production dependencies +RUN npm ci --only=production && npm cache clean --force + +# Copy built application from build stage +COPY --from=build /app/dist ./dist + +EXPOSE 8007 + +CMD ["npm", "start"] \ No newline at end of file diff --git a/server/package.json b/server/package.json index 8fb5beb..5831883 100644 --- a/server/package.json +++ b/server/package.json @@ -1,7 +1,6 @@ { "name": "note-manager-server", "version": "1.0.0", - "type": "module", "scripts": { "dev": "nodemon --exec tsx src/app.ts", "build": "tsc", diff --git a/server/src/app.ts b/server/src/app.ts index 7a2c13b..5e7c97d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -15,7 +15,7 @@ connectDB(); app.use(helmet()); app.use( cors({ - origin: envConfig.CORS_ORIGIN, + origin: envConfig.CORS_ORIGIN || true, credentials: true, methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], diff --git a/server/tsconfig.json b/server/tsconfig.json index 5cf5668..8e4769b 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,8 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "module": "ESNext", - "moduleResolution": "node", + "module": "commonjs", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, @@ -15,7 +14,7 @@ "sourceMap": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noImplicitReturns": true, + "noImplicitReturns": false, "noFallthroughCasesInSwitch": true }, "include": [