diff --git a/sk1/.gitignore b/sk1/.gitignore new file mode 100644 index 0000000..24249a3 --- /dev/null +++ b/sk1/.gitignore @@ -0,0 +1,11 @@ +# Secrets and environment variables +k8s/mongo-secret.yml +.env +*/.env +**/.env +.env.* + +# Node modules and build artifacts +node_modules/ +dist/ +build/ \ No newline at end of file diff --git a/sk1/README.md b/sk1/README.md new file mode 100755 index 0000000..4064cae --- /dev/null +++ b/sk1/README.md @@ -0,0 +1,67 @@ +# Nudges.works — GKE Deployment on GCP + +## What it does +A service marketplace application that connects users with service providers. Users can search for professionals like plumbers, electricians, or gardeners in their area and view their profiles, including contact information and rates. + +## Architecture +- GKE cluster (3 nodes) running on Google Cloud +- Google-managed TLS certificate for HTTPS +- Ingress controller for traffic routing +- 2 Deployments & Services: `frontend` (React/Vite), `backend` (Express) +- MongoDB hosted on Atlas (external persistent database) +- Docker images stored in Google Container Registry (GCR) + +## Files +- `backend/Dockerfile` - Containerizes the Express backend +- `frontend/Dockerfile` - Containerizes the React frontend +- `frontend/nginx.conf` - Nginx configuration for serving frontend and proxying API requests +- `k8s/` - Kubernetes manifests: + - `mongo-secret.yml` - Secret for MongoDB credentials and API keys + - `backend-deployment.yml.tpl` - Backend deployment template + - `backend-service.yml` - Backend service + - `backend-config.yml` - Health check configuration for backend + - `frontend-deployment.yml.tpl` - Frontend deployment template + - `frontend-service.yml` - Frontend service + - `managed-cert.yml` - GCP-managed TLS certificate + - `ingress.yml` - Ingress configuration for routing +- `prepare-app.sh` - Script to deploy everything +- `remove-app.sh` - Script to tear down the deployment +- `README.md` - This documentation + +## Prerequisites +- Google Cloud SDK + billing enabled +- Install the GKE auth plugin: + ```bash + gcloud components install gke-gcloud-auth-plugin + ``` +- Ensure your shell sees the Linux `gcloud`, `kubectl` and `npm`, not Windows versions +- `gcloud init` & set project +- Enable APIs: Container, Compute, Cloud Build + +## DNS setup +1. After `./prepare-app.sh` finishes you'll see the ingress external IP +2. In your DNS provider, set A-record for `nudges.works` → your GKE Ingress EXTERNAL-IP. +3. (Optional) CNAME `www` → `nudges.works`. + +## Deploy +```bash +chmod +x prepare-app.sh remove-app.sh +./prepare-app.sh +``` +Wait until `ManagedCertificate` → `Active`. Then browse +https://nudges.works + +## Note on Certificate Provisioning +- The managed certificate can take 30-60 minutes to provision +- Your application will be accessible via HTTP immediately, but HTTPS requires waiting for certificate activation +- You can check certificate status with: `kubectl get managedcertificates -n sk1` + +## Teardown +```bash +./remove-app.sh +``` + +## External Sources +- GKE ManagedCertificate guide +- MongoDB Atlas docs +- Generated with GitHub Copilot \ No newline at end of file diff --git a/sk1/backend/Dockerfile b/sk1/backend/Dockerfile new file mode 100644 index 0000000..f54d252 --- /dev/null +++ b/sk1/backend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 4000 + +CMD ["node", "index.js"] \ No newline at end of file diff --git a/sk1/backend/config/db.js b/sk1/backend/config/db.js new file mode 100644 index 0000000..cfca360 --- /dev/null +++ b/sk1/backend/config/db.js @@ -0,0 +1,11 @@ +import mongoose from "mongoose"; + +export async function connectDB() { + try { + await mongoose.connect(process.env.MONGO_URI); + console.log("✅ MongoDB connected"); + } catch (err) { + console.error("❌ MongoDB connection failed:", err); + process.exit(1); + } +} diff --git a/sk1/backend/index.js b/sk1/backend/index.js new file mode 100644 index 0000000..e72e4c1 --- /dev/null +++ b/sk1/backend/index.js @@ -0,0 +1,30 @@ +import express from "express"; +import cors from "cors"; +import dotenv from "dotenv"; +import { connectDB } from "./config/db.js"; +import authRoutes from "./routes/auth.js"; +import profilesRoutes from "./routes/profiles.js"; +import servicesRoutes from "./routes/services.js"; +import autocompleteRoutes from "./routes/autocomplete.js"; + +dotenv.config(); +await connectDB(); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +app.use("/api/auth", authRoutes); +app.use("/api/profiles", profilesRoutes); +app.use("/api/services", servicesRoutes); +app.use("/api/autocomplete", autocompleteRoutes); + +// Add a health check endpoint for Kubernetes +app.get("/api", (req, res) => { + res.status(200).json({ status: "ok", message: "API is healthy" }); +}); + +const PORT = process.env.PORT || 4000; +app.listen(PORT, '0.0.0.0', () => + console.log(`🚀 Server running on http://0.0.0.0:${PORT}`) +); diff --git a/sk1/backend/middleware/auth.js b/sk1/backend/middleware/auth.js new file mode 100644 index 0000000..8dc30b2 --- /dev/null +++ b/sk1/backend/middleware/auth.js @@ -0,0 +1,28 @@ +import jwt from "jsonwebtoken"; + +// require a valid JWT in Authorization: Bearer +export function requireAuth(req, res, next) { + const auth = req.headers.authorization || ""; + const [scheme, token] = auth.split(" "); + if (scheme !== "Bearer" || !token) { + return res.status(401).json({ error: "Not authenticated" }); + } + try { + const payload = jwt.verify(token, process.env.JWT_SECRET); + req.user = { id: payload.sub, role: payload.role }; + next(); + } catch { + res.status(401).json({ error: "Invalid token" }); + } +} + +// ensure the user has a specific role +export function requireRole(role) { + return (req, res, next) => { + if (!req.user) return res.status(401).json({ error: "Not authenticated" }); + if (req.user.role !== role) { + return res.status(403).json({ error: "Forbidden" }); + } + next(); + }; +} diff --git a/sk1/backend/models/User.js b/sk1/backend/models/User.js new file mode 100644 index 0000000..d57d973 --- /dev/null +++ b/sk1/backend/models/User.js @@ -0,0 +1,40 @@ +import mongoose from "mongoose"; +import bcrypt from "bcrypt"; + +const profileSchema = new mongoose.Schema({ + firstName: String, + lastName: String, + phone: String, + field: String, + rate: Number, + address: String, + city: String, + location: { + type: { type: String, enum: ["Point"], default: "Point" }, + coordinates: { type: [Number], index: "2dsphere" }, + }, + published: { type: Boolean, default: false }, + description: String, // ← new + picture: String, // ← new (URL or upload path) +}); + +const userSchema = new mongoose.Schema({ + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + role: { type: String, enum: ["normal", "pro"], default: "normal" }, + profile: { type: profileSchema, default: {} }, +}); + +// hash password before save +userSchema.pre("save", async function () { + if (!this.isModified("password")) return; + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); +}); + +// helper to compare passwords +userSchema.methods.comparePassword = function (plain) { + return bcrypt.compare(plain, this.password); +}; + +export const User = mongoose.model("User", userSchema); diff --git a/sk1/backend/package.json b/sk1/backend/package.json new file mode 100644 index 0000000..3008d03 --- /dev/null +++ b/sk1/backend/package.json @@ -0,0 +1,27 @@ +{ + "name": "backend", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "nodemon index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "bcrypt": "^5.1.1", + "bcryptjs": "^3.0.2", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.14.1", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } +} diff --git a/sk1/backend/routes/auth.js b/sk1/backend/routes/auth.js new file mode 100644 index 0000000..6fac915 --- /dev/null +++ b/sk1/backend/routes/auth.js @@ -0,0 +1,45 @@ +import express from "express"; +import jwt from "jsonwebtoken"; +import { User } from "../models/User.js"; + +const router = express.Router(); + +// POST /api/auth/register +router.post("/register", async (req, res) => { + const { email, password, role } = req.body; + try { + const exists = await User.findOne({ email }); + if (exists) return res.status(409).json({ error: "Email in use" }); + const user = new User({ email, password, role }); + await user.save(); + const token = jwt.sign( + { sub: user._id, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ); + res.json({ token }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// POST /api/auth/login +router.post("/login", async (req, res) => { + const { email, password } = req.body; + try { + const user = await User.findOne({ email }); + if (!user || !(await user.comparePassword(password))) { + return res.status(401).json({ error: "Invalid credentials" }); + } + const token = jwt.sign( + { sub: user._id, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ); + res.json({ token }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +export default router; diff --git a/sk1/backend/routes/autocomplete.js b/sk1/backend/routes/autocomplete.js new file mode 100644 index 0000000..9e50cf0 --- /dev/null +++ b/sk1/backend/routes/autocomplete.js @@ -0,0 +1,26 @@ +import express from "express"; +import fetch from "node-fetch"; + +const router = express.Router(); + +// GET /api/autocomplete?input=... +router.get("/", async (req, res) => { + const { input } = req.query; + if (!input) return res.status(400).json({ predictions: [] }); + + const url = + `https://maps.googleapis.com/maps/api/place/autocomplete/json` + + `?input=${encodeURIComponent(input)}` + + `&key=${process.env.GOOGLE_PLACES_API_KEY}` + + `&types=address`; + + try { + const r = await fetch(url); + const data = await r.json(); + res.json(data); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +export default router; diff --git a/sk1/backend/routes/profiles.js b/sk1/backend/routes/profiles.js new file mode 100644 index 0000000..5f9d28d --- /dev/null +++ b/sk1/backend/routes/profiles.js @@ -0,0 +1,186 @@ +import express from "express"; +import fetch from "node-fetch"; +import bcrypt from "bcryptjs"; +import { User } from "../models/User.js"; +import { requireAuth } from "../middleware/auth.js"; + +const router = express.Router(); + +// GET /api/profiles/me +router.get("/me", requireAuth, async (req, res) => { + const user = await User.findById(req.user.id).select("-password -__v"); + if (!user) return res.status(404).json({ error: "User not found" }); + res.json({ + email: user.email, + role: user.role, + profile: user.profile, + }); +}); + +// NEW: GET any profile by its ID +router.get("/:id", async (req, res) => { + try { + const user = await User.findById(req.params.id).select("-password -__v"); + if (!user) return res.status(404).json({ error: "Profile not found" }); + return res.json({ + email: user.email, + role: user.role, + profile: user.profile, + }); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +// POST /api/profiles (create or update profile) +// – normal user: only name/address/password +// – pro user: full profile (field, rate, address, lat, lon, published) +router.post("/", requireAuth, async (req, res) => { + try { + const user = await User.findById(req.user.id); + + if (req.user.role === "pro") { + const { + firstName, + lastName, + phone, + field, + rate, + address, + description, + picture, + published, + } = req.body; + + // pure publish toggle? + if ( + req.body.hasOwnProperty("published") && + Object.keys(req.body).length === 1 + ) { + // disallow publishing unless all fields are set + if (published) { + const pf = (await User.findById(req.user.id)).profile; + if ( + !pf.firstName || + !pf.lastName || + !pf.phone || + !pf.field || + pf.rate == null || + !pf.address || + !pf.description || // ← require description + !pf.picture // ← require picture + ) { + return res.status(400).json({ + error: + "Complete firstName, lastName, phone, field, rate, address, description & picture before publishing.", + }); + } + } + user.profile.published = !!published; + await user.save(); + return res.json({ success: true }); + } + + // full update → geocode + persist all fields + const geoRes = await fetch( + `https://maps.googleapis.com/maps/api/geocode/json` + + `?address=${encodeURIComponent(address)}` + + `&key=${process.env.GOOGLE_PLACES_API_KEY}` + ); + const geoData = await geoRes.json(); + console.log( + "🗺️ geoData.results:", + JSON.stringify(geoData.results, null, 2) + ); + if (geoData.status !== "OK" || !geoData.results.length) { + return res.status(400).json({ error: "Invalid address" }); + } + + // prefer a true city‐level result, then region, then country, then anything + const place = + geoData.results.find((r) => r.types.includes("locality")) || + geoData.results.find((r) => + r.types.includes("administrative_area_level_2") + ) || + geoData.results.find((r) => + r.types.includes("administrative_area_level_1") + ) || + geoData.results[0]; + + const { lat, lng } = place.geometry.location; + + // first try the locality component + const cityComp = place.address_components.find((c) => + c.types.includes("locality") + ); + let rawCity; + if (cityComp) { + rawCity = cityComp.long_name; + } else { + // use the penultimate segment of formatted_address, e.g. + // ["Hlavná","Old Town","Košice","Slovakia"] → "Košice" + const parts = (place.formatted_address || "") + .split(",") + .map((s) => s.trim()); + rawCity = parts.length > 1 ? parts[parts.length - 2] : parts[0]; + } + + // normalize: strip accents + lowercase + const city = rawCity + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); + + // now persist everything + user.profile.firstName = firstName; + user.profile.lastName = lastName; + user.profile.phone = phone; + user.profile.field = field; + user.profile.rate = rate; + user.profile.address = address; + user.profile.description = description; // ← save description + user.profile.picture = picture; // ← save picture + user.profile.city = city; + user.profile.location = { + type: "Point", + coordinates: [lng, lat], + }; + user.profile.published = !!published; + } else { + // ─── Normal user profile update ────────────────────────────────────── + // Allow them to save firstName, lastName, phone, and (optionally) password + const { firstName, lastName, phone, password } = req.body; + const u = await User.findById(req.user.id); + if (!u) return res.status(404).json({ error: "User not found" }); + + if (firstName !== undefined) u.profile.firstName = firstName; + if (lastName !== undefined) u.profile.lastName = lastName; + if (phone !== undefined) u.profile.phone = phone; + // if they submitted a new password, hash + save it + if (password) { + u.password = await bcrypt.hash(password, 10); + } + + await u.save(); + return res.json({ success: true, profile: u.profile }); + } + + await user.save(); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// DELETE /api/profiles/me → unpublish pro or delete normal account +router.delete("/me", requireAuth, async (req, res) => { + try { + // Delete the entire user account + await User.findByIdAndDelete(req.user.id); + return res.json({ success: true }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +export default router; diff --git a/sk1/backend/routes/services.js b/sk1/backend/routes/services.js new file mode 100644 index 0000000..7a21671 --- /dev/null +++ b/sk1/backend/routes/services.js @@ -0,0 +1,90 @@ +import express from "express"; +import { User } from "../models/User.js"; + +const router = express.Router(); + +router.get("/:field", async (req, res) => { + const { field } = req.params; + const lat = parseFloat(req.query.lat); + const lon = parseFloat(req.query.lon); + if (isNaN(lat) || isNaN(lon)) { + return res.status(400).json({ error: "Must provide lat & lon" }); + } + + const rawRadius = parseFloat(req.query.radius); + const radiusKm = isNaN(rawRadius) || rawRadius < 0 ? 50 : rawRadius; + const maxDistance = radiusKm * 1000; + const page = Math.max(1, parseInt(req.query.page) || 1); + const limit = 6; // now 6 per page + const skip = (page - 1) * limit; + + const sortBy = req.query.sortBy === "rate" ? "rate" : "distance"; + const order = req.query.order === "desc" ? -1 : 1; + + try { + // pipeline to get paged docs + const pipeline = [ + { + $geoNear: { + near: { type: "Point", coordinates: [lon, lat] }, + distanceField: "distanceMeters", + maxDistance, + spherical: true, + }, + }, + { + $match: { + "profile.field": field, + "profile.published": true, + }, + }, + { + $sort: + sortBy === "rate" + ? { "profile.rate": order } + : { distanceMeters: order }, + }, + { $skip: skip }, + { $limit: limit }, + ]; + + // pipeline to count total matching + const countPipeline = [ + { + $geoNear: { + near: { type: "Point", coordinates: [lon, lat] }, + distanceField: "distanceMeters", + maxDistance, + spherical: true, + }, + }, + { + $match: { + "profile.field": field, + "profile.published": true, + }, + }, + { $count: "count" }, + ]; + + const [docs, countRes] = await Promise.all([ + User.aggregate(pipeline), + User.aggregate(countPipeline), + ]); + + const total = countRes[0]?.count || 0; + const totalPages = Math.ceil(total / limit); + + const pros = docs.map((u) => ({ + _id: u._id, + profile: u.profile, + distanceKm: u.distanceMeters / 1000, + })); + + return res.json({ pros, page, totalPages }); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +export default router; diff --git a/sk1/fixed-backend.sh b/sk1/fixed-backend.sh new file mode 100755 index 0000000..50c6c8b --- /dev/null +++ b/sk1/fixed-backend.sh @@ -0,0 +1,63 @@ +# Create a fixed deployment file +cat > fixed-backend.yml << EOF +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: sk1 +spec: + replicas: 2 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: backend + image: gcr.io/$(gcloud config get-value project)/backend:latest + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "200m" + memory: "256Mi" + ports: + - containerPort: 4000 + env: + - name: PORT + value: "4000" + - name: MONGO_URI + valueFrom: + secretKeyRef: + name: mongodb-secret + key: MONGO_URI + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: mongodb-secret + key: JWT_SECRET + - name: GOOGLE_PLACES_API_KEY + valueFrom: + secretKeyRef: + name: mongodb-secret + key: GOOGLE_PLACES_API_KEY + livenessProbe: + httpGet: + path: /api + port: 4000 + initialDelaySeconds: 30 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /api + port: 4000 + initialDelaySeconds: 15 + periodSeconds: 10 +EOF + +# Apply the fixed deployment +kubectl apply -f fixed-backend.yml \ No newline at end of file diff --git a/sk1/fixed-backend.yml b/sk1/fixed-backend.yml new file mode 100644 index 0000000..940f2cc --- /dev/null +++ b/sk1/fixed-backend.yml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: sk1 +spec: + replicas: 2 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: backend + image: gcr.io/leafy-racer-458817-c0/backend:latest + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "200m" + memory: "256Mi" + ports: + - containerPort: 4000 + env: + - name: PORT + value: "4000" + - name: MONGO_URI + valueFrom: + secretKeyRef: + name: mongodb-secret + key: MONGO_URI + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: mongodb-secret + key: JWT_SECRET + - name: GOOGLE_PLACES_API_KEY + valueFrom: + secretKeyRef: + name: mongodb-secret + key: GOOGLE_PLACES_API_KEY + livenessProbe: + httpGet: + path: /api + port: 4000 + initialDelaySeconds: 30 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /api + port: 4000 + initialDelaySeconds: 15 + periodSeconds: 10 diff --git a/sk1/frontend/.gitignore b/sk1/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/sk1/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/sk1/frontend/Dockerfile b/sk1/frontend/Dockerfile new file mode 100755 index 0000000..ba1f7ca --- /dev/null +++ b/sk1/frontend/Dockerfile @@ -0,0 +1,26 @@ +FROM node:20-alpine as build + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +RUN npm run build + +FROM nginx:alpine + +# Copy built files to nginx +COPY --from=build /app/dist /usr/share/nginx/html + +# Copy nginx config - keep only this line, remove the duplicate below +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +# REMOVE THIS DUPLICATE LINE +# COPY nginx/default.conf /etc/nginx/conf.d/default.conf + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/sk1/frontend/README.md b/sk1/frontend/README.md new file mode 100755 index 0000000..7059a96 --- /dev/null +++ b/sk1/frontend/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/sk1/frontend/eslint.config.js b/sk1/frontend/eslint.config.js new file mode 100755 index 0000000..ec2b712 --- /dev/null +++ b/sk1/frontend/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/sk1/frontend/fix-config.sh b/sk1/frontend/fix-config.sh new file mode 100755 index 0000000..140d72b --- /dev/null +++ b/sk1/frontend/fix-config.sh @@ -0,0 +1,27 @@ +mkdir -p frontend/nginx + +# Create a proper nginx.conf file for frontend +cat > frontend/nginx/default.conf << 'EOF' +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # API requests should be forwarded to the backend service + location /api/ { + proxy_pass http://backend-service:4000/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_cache_bypass $http_upgrade; + } + + # All other requests go to the React app + location / { + try_files $uri $uri/ /index.html; + } +} +EOF \ No newline at end of file diff --git a/sk1/frontend/frontend/nginx/default.conf b/sk1/frontend/frontend/nginx/default.conf new file mode 100644 index 0000000..86b0545 --- /dev/null +++ b/sk1/frontend/frontend/nginx/default.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # API requests should be forwarded to the backend service + location /api/ { + proxy_pass http://backend-service:4000/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_cache_bypass $http_upgrade; + } + + # All other requests go to the React app + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/sk1/frontend/index.html b/sk1/frontend/index.html new file mode 100755 index 0000000..38e4d5f --- /dev/null +++ b/sk1/frontend/index.html @@ -0,0 +1,12 @@ + + + + + Nudge Job Portal + + + +
+ + + diff --git a/sk1/frontend/nginx.conf b/sk1/frontend/nginx.conf new file mode 100755 index 0000000..1b0f346 --- /dev/null +++ b/sk1/frontend/nginx.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # API requests should be forwarded to the backend service + location /api/ { + proxy_pass http://backend-service.sk1.svc.cluster.local:4000/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_cache_bypass $http_upgrade; + } + + # All other requests go to the React app + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/sk1/frontend/nginx/default.conf b/sk1/frontend/nginx/default.conf new file mode 100644 index 0000000..86b0545 --- /dev/null +++ b/sk1/frontend/nginx/default.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # API requests should be forwarded to the backend service + location /api/ { + proxy_pass http://backend-service:4000/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_cache_bypass $http_upgrade; + } + + # All other requests go to the React app + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/sk1/frontend/package.json b/sk1/frontend/package.json new file mode 100755 index 0000000..5baabb2 --- /dev/null +++ b/sk1/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.5.3" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.22.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "vite": "^6.3.1" + } +} diff --git a/sk1/frontend/public/vite.svg b/sk1/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/sk1/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sk1/frontend/someshit.sh b/sk1/frontend/someshit.sh new file mode 100755 index 0000000..2c6766c --- /dev/null +++ b/sk1/frontend/someshit.sh @@ -0,0 +1,6 @@ +# Update Dockerfile to use the nginx.conf +if [ -f frontend/Dockerfile ]; then + if ! grep -q "nginx.conf" frontend/Dockerfile; then + sed -i '/CMD/i COPY nginx.conf /etc/nginx/conf.d/default.conf' frontend/Dockerfile + fi +fi \ No newline at end of file diff --git a/sk1/frontend/src/App.css b/sk1/frontend/src/App.css new file mode 100644 index 0000000..3fb3548 --- /dev/null +++ b/sk1/frontend/src/App.css @@ -0,0 +1,42 @@ +/* #root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} */ diff --git a/sk1/frontend/src/App.jsx b/sk1/frontend/src/App.jsx new file mode 100644 index 0000000..863c4fe --- /dev/null +++ b/sk1/frontend/src/App.jsx @@ -0,0 +1,34 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import Nav from "./components/Nav"; +import Footer from "./components/Footer"; +import Home from "./pages/Home"; +import LoginPage from "./pages/LoginPage"; +import SignUpPage from "./pages/SignUpPage"; +import ServicePage from "./pages/ServicePage"; +import ProfilePage from "./pages/ProfilePage"; +import PublishProfilePage from "./pages/PublishProfilePage"; +import ProtectedRoute from "./components/ProtectedRoute"; + +export default function App() { + return ( + +