diff --git a/node-mongodb/.dockerignore b/node-mongodb/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/node-mongodb/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/node-mongodb/.gitignore b/node-mongodb/.gitignore new file mode 100644 index 0000000..dffbb2b --- /dev/null +++ b/node-mongodb/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.env +/node_modules/ +*.tsbuildinfo + +# React Router +/.react-router/ +/build/ diff --git a/node-mongodb/Dockerfile b/node-mongodb/Dockerfile new file mode 100644 index 0000000..d7526ff --- /dev/null +++ b/node-mongodb/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json server.js /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/node-mongodb/README.md b/node-mongodb/README.md new file mode 100644 index 0000000..6215fdd --- /dev/null +++ b/node-mongodb/README.md @@ -0,0 +1,86 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router. + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +### Docker Deployment + +To build and run using Docker: + +```bash +docker build -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── server.js +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/node-mongodb/app/app.css b/node-mongodb/app/app.css new file mode 100644 index 0000000..99345d8 --- /dev/null +++ b/node-mongodb/app/app.css @@ -0,0 +1,15 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/node-mongodb/app/model/todo.ts b/node-mongodb/app/model/todo.ts new file mode 100644 index 0000000..7c9cde0 --- /dev/null +++ b/node-mongodb/app/model/todo.ts @@ -0,0 +1,60 @@ +import { Schema, model, Document } from "mongoose"; + +interface ITodo extends Document { + _id: string; + title: string; + description?: string; + completed: boolean; + createdAt: Date; + updatedAt: Date; +} + +// Also create a plain version for serialized data +interface ITodoPlain { + _id: string; + title: string; + description?: string; + completed: boolean; + createdAt: Date; + updatedAt: Date; +} + +const todoSchema = new Schema( + { + title: { + type: String, + required: true, + trim: true, + }, + description: { + type: String, + trim: true, + }, + completed: { + type: Boolean, + default: false, + }, + }, + { + timestamps: true, + toJSON: { + transform: function (doc, ret) { + if (ret._id) { + ret._id = ret._id.toString(); + } + return ret; + }, + }, + toObject: { + transform: function (doc, ret) { + if (ret._id) { + ret._id = ret._id.toString(); + } + return ret; + }, + }, + } +); + +export const Todo = model("Todo", todoSchema); +export type { ITodo, ITodoPlain }; diff --git a/node-mongodb/app/root.tsx b/node-mongodb/app/root.tsx new file mode 100644 index 0000000..9fc6636 --- /dev/null +++ b/node-mongodb/app/root.tsx @@ -0,0 +1,75 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +import type { Route } from "./+types/root"; +import "./app.css"; + +export const links: Route.LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/node-mongodb/app/routes.ts b/node-mongodb/app/routes.ts new file mode 100644 index 0000000..ac4fb00 --- /dev/null +++ b/node-mongodb/app/routes.ts @@ -0,0 +1,9 @@ +import { type RouteConfig, index, route } from "@react-router/dev/routes"; + +export default [ + index("routes/home.tsx"), + route("/todos", "routes/todos/index.tsx"), + route("/todos/new", "routes/todos/create.tsx"), + route("/todos/:id/toggle", "routes/todos/toggle.tsx"), + route("/todos/:id/delete", "routes/todos/delete.tsx"), +] satisfies RouteConfig; diff --git a/node-mongodb/app/routes/home.tsx b/node-mongodb/app/routes/home.tsx new file mode 100644 index 0000000..4dd1316 --- /dev/null +++ b/node-mongodb/app/routes/home.tsx @@ -0,0 +1,17 @@ +import type { Route } from "./+types/home"; +import { Welcome } from "../welcome/welcome"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, + ]; +} + +export function loader() { + return { message: "MongoDB template" }; +} + +export default function Home({ loaderData }: Route.ComponentProps) { + return ; +} diff --git a/node-mongodb/app/routes/todos/create.tsx b/node-mongodb/app/routes/todos/create.tsx new file mode 100644 index 0000000..190a748 --- /dev/null +++ b/node-mongodb/app/routes/todos/create.tsx @@ -0,0 +1,122 @@ +import mongoose from "mongoose"; +import { redirect } from "react-router"; +import type { Route } from "./+types/create"; +import { Todo } from "~/model/todo"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "Create Todo - React Router App" }, + { name: "description", content: "Create a new todo item" }, + ]; +} + +export async function action({ request }: Route.ActionArgs) { + if (mongoose.connection.readyState !== 1) { + await mongoose.connect(process.env.MONGODB_URI!); + } + + const formData = await request.formData(); + const title = formData.get("title") as string; + const description = formData.get("description") as string; + + if (!title || title.trim().length === 0) { + return { + error: "Title is required", + values: { title: "", description: description || "" }, + }; + } + + try { + await Todo.create({ + title: title.trim(), + description: description?.trim() || "", + }); + + return redirect("/todos"); + } catch (error) { + console.error("Failed to create todo:", error); + return { + error: "Failed to create todo. Please try again.", + values: { title, description: description || "" }, + }; + } +} + +export default function CreateTodo({ actionData }: Route.ComponentProps) { + return ( +
+
+
+

+ Create New Todo +

+

+ Add a new task to your todo list +

+
+ +
+
+
+ + +
+ +
+ +