diff --git a/day16/.gitignore b/day16/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/day16/.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/day16/eslint.config.js b/day16/eslint.config.js
new file mode 100644
index 0000000..cee1e2c
--- /dev/null
+++ b/day16/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs['recommended-latest'],
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ },
+ },
+])
diff --git a/day16/index.html b/day16/index.html
new file mode 100644
index 0000000..0c589ec
--- /dev/null
+++ b/day16/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vite + React
+
+
+
+
+
+
diff --git a/day16/package.json b/day16/package.json
new file mode 100644
index 0000000..7c27471
--- /dev/null
+++ b/day16/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "day16",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.1.11",
+ "fabric": "^6.7.0",
+ "fabricjs-react": "^1.2.2",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.30.1",
+ "@types/react": "^19.1.8",
+ "@types/react-dom": "^19.1.6",
+ "@vitejs/plugin-react": "^4.6.0",
+ "autoprefixer": "^10.4.21",
+ "eslint": "^9.30.1",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "globals": "^16.3.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^4.1.11",
+ "vite": "^7.0.4"
+ }
+}
diff --git a/day16/public/vite.svg b/day16/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/day16/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/day16/src/App.css b/day16/src/App.css
new file mode 100644
index 0000000..f1d8c73
--- /dev/null
+++ b/day16/src/App.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/day16/src/App.jsx b/day16/src/App.jsx
new file mode 100644
index 0000000..fead0f9
--- /dev/null
+++ b/day16/src/App.jsx
@@ -0,0 +1,317 @@
+import React, { useRef, useEffect, useState } from "react";
+import { Canvas, Textbox, Image } from "fabric";
+
+function App() {
+ const canvasRef = useRef(null);
+ const fabricRef = useRef(null);
+ const [activeMenu, setActiveMenu] = useState("Background");
+ const [selectedObject, setSelectedObject] = useState(null);
+ const [fontSize, setFontSize] = useState(60);
+ const [images, setImages] = useState([]);
+ const [isImagesLoading, setIsImagesLoading] = useState(false);
+
+ const colors = [
+ "#F4A261",
+ "#E76F51",
+ "#2A9D8F",
+ "#264653",
+ "#A8DADC",
+ "#457B9D",
+ "#E63946",
+ "#F7B801",
+ "#A259F7",
+ "#6D6875",
+ ];
+
+ useEffect(() => {
+ const canvas = new Canvas(canvasRef.current, {
+ width: 900,
+ height: 400,
+ backgroundColor: "#F4A261",
+ });
+ fabricRef.current = canvas;
+
+ // Object selection logic
+ function updateSelectedObject() {
+ setSelectedObject(canvas.getActiveObject());
+ const obj = canvas.getActiveObject();
+ if (obj && obj.type === "text") {
+ setFontSize(obj.fontSize);
+ }
+ }
+ canvas.on("selection:created", updateSelectedObject);
+ canvas.on("selection:updated", updateSelectedObject);
+ canvas.on("selection:cleared", updateSelectedObject);
+
+ // Allow editing text on double click
+ canvas.on("mouse:dblclick", (e) => {
+ if (e.target && e.target.type === "text") {
+ e.target.enterEditing();
+ e.target.selectAll();
+ }
+ });
+
+ // Force initial render so canvas is visible
+ canvas.requestRenderAll();
+
+ return () => {
+ canvas.dispose();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (images.length === 0) {
+ setIsImagesLoading(true);
+ fetch("https://picsum.photos/v2/list")
+ .then((res) => res.json())
+ .then((data) => setImages(data))
+ .catch((err) => console.error("Failed to fetch images", err))
+ .finally(() => setIsImagesLoading(false));
+ }
+ }, [images]);
+
+ // Add text to canvas
+ const addText = (type) => {
+ let text = "";
+ let size = 60;
+ if (type === "h1") {
+ text = "Add a heading";
+ size = 60;
+ } else if (type === "h6") {
+ text = "Add a subheading";
+ size = 32;
+ } else {
+ text = "Add a little bit of body text";
+ size = 20;
+ }
+ const canvas = fabricRef.current;
+ const textbox = new Textbox(text, {
+ left: canvas.width / 2,
+ top: canvas.height / 2,
+ fontSize: size,
+ fontWeight: type === "h1" ? "bold" : "normal",
+ editable: true,
+ originX: "center",
+ originY: "center",
+ fill: "#222",
+ });
+ canvas.add(textbox).setActiveObject(textbox);
+ canvas.renderAll();
+ };
+
+ // Add image to canvas
+ const addImageToCanvas = async (url) => {
+ const img = await Image.fromURL(url, { crossOrigin: "anonymous" });
+ const canvas = fabricRef.current;
+ img.scaleToWidth(200);
+ img.set({
+ left: canvas.width / 2,
+ top: canvas.height / 2,
+ originX: "center",
+ originY: "center",
+ });
+ canvas.add(img);
+ canvas.setActiveObject(img);
+ canvas.requestRenderAll();
+ };
+
+ // Set background color
+ const setBackgroundColor = (color) => {
+ if (fabricRef.current) {
+ fabricRef.current.backgroundColor = color;
+ fabricRef.current.requestRenderAll();
+ }
+ };
+
+ // Download canvas
+ const downloadCanvas = () => {
+ if (fabricRef.current) {
+ const dataURL = fabricRef.current.toDataURL({
+ format: "png",
+ quality: 1,
+ });
+ const link = document.createElement("a");
+ link.href = dataURL;
+ link.download = "canvas.png";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+ };
+
+ // Change font size of selected text
+ const handleFontSizeChange = (e) => {
+ const size = parseInt(e.target.value, 10);
+ setFontSize(size);
+ if (selectedObject && selectedObject.type === "text") {
+ selectedObject.set({ fontSize: size });
+ fabricRef.current.renderAll();
+ }
+ };
+
+ // Delete selected object (text or image)
+ const deleteSelectedObject = () => {
+ if (selectedObject) {
+ fabricRef.current.remove(selectedObject);
+ setSelectedObject(null);
+ }
+ };
+
+ return (
+
+ {/* Navbar */}
+
+
+ {/* Sidebar */}
+
+ {/* Canvas Area */}
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/day16/src/assets/react.svg b/day16/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/day16/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/day16/src/index.css b/day16/src/index.css
new file mode 100644
index 0000000..1856baf
--- /dev/null
+++ b/day16/src/index.css
@@ -0,0 +1,7 @@
+@import "tailwindcss";
+
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+}
diff --git a/day16/src/main.jsx b/day16/src/main.jsx
new file mode 100644
index 0000000..b9a1a6d
--- /dev/null
+++ b/day16/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/day16/vite.config.js b/day16/vite.config.js
new file mode 100644
index 0000000..4ff4f8f
--- /dev/null
+++ b/day16/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+});
diff --git a/day17/app.js b/day17/app.js
index 97a1728..b87197d 100644
--- a/day17/app.js
+++ b/day17/app.js
@@ -1,11 +1,11 @@
-var createError = require('http-errors');
-var express = require('express');
-var path = require('path');
-var cookieParser = require('cookie-parser');
-var logger = require('morgan');
+var createError = require("http-errors");
+var express = require("express");
+var path = require("path");
+var cookieParser = require("cookie-parser");
+var logger = require("morgan");
-var indexRouter = require('./routes/index');
-var usersRouter = require('./routes/users');
+var indexRouter = require("./routes/index");
+var usersRouter = require("./routes/users");
const db = require("./models");
var cors = require("cors");
@@ -13,17 +13,17 @@ var cors = require("cors");
var app = express();
app.set("db", db);
// view engine setup
-app.set('views', path.join(__dirname, 'views'));
-app.set('view engine', 'jade');
+app.set("views", path.join(__dirname, "views"));
+app.set("view engine", "ejs");
app.use(cors());
-app.use(logger('dev'));
+app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
-app.use(express.static(path.join(__dirname, 'public')));
+app.use(express.static(path.join(__dirname, "public")));
-app.use('/', indexRouter);
-app.use('/users', usersRouter);
+app.use("/", indexRouter);
+app.use("/users", usersRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
@@ -34,11 +34,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
- res.locals.error = req.app.get('env') === 'development' ? err : {};
+ res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
- res.render('error');
+ res.render("error");
});
module.exports = app;
diff --git a/day17/package.json b/day17/package.json
index 219c20e..af64762 100644
--- a/day17/package.json
+++ b/day17/package.json
@@ -3,15 +3,18 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "start": "node ./bin/www"
+ "start": "node ./bin/www",
+ "dev": "node --watch --env-file=.env ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
+ "ejs": "^3.1.10",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
+ "moment-timezone": "^0.6.0",
"morgan": "~1.9.1",
"mysql2": "^2.3.3",
"sequelize": "^6.15.1"
diff --git a/day17/routes/index.js b/day17/routes/index.js
index ecca96a..228bf0b 100644
--- a/day17/routes/index.js
+++ b/day17/routes/index.js
@@ -1,9 +1,125 @@
-var express = require('express');
+var express = require("express");
var router = express.Router();
+const moment = require("moment-timezone");
-/* GET home page. */
-router.get('/', function(req, res, next) {
- res.render('index', { title: 'Express' });
+// Helper: Group timezones by region for the timezone selection screen
+function getTimezoneGroups() {
+ const regions = {
+ "USA/CANADA": [
+ "America/Los_Angeles",
+ "America/Denver",
+ "America/New_York",
+ "America/Halifax",
+ ],
+ EUROPE: [
+ "Europe/Berlin",
+ "Europe/Helsinki",
+ "Europe/Dublin",
+ "Europe/Samara",
+ ],
+ ASIA: ["Asia/Hong_Kong", "Asia/Jakarta", "Asia/Kabul", "Asia/Kathmandu"],
+ "SOUTH AMERICA": [
+ "America/Bogota",
+ "America/Campo_Grande",
+ "America/Caracas",
+ "America/Lima",
+ ],
+ };
+ const groups = {};
+ for (const region in regions) {
+ groups[region] = regions[region].map((tz) => ({
+ value: tz,
+ label: moment().tz(tz).zoneAbbr() + " " + moment().tz(tz).format("h:mma"),
+ }));
+ }
+ return groups;
+}
+
+// Helper: All timezones for dropdown
+function getAllTimezones() {
+ return moment.tz.names().map((tz) => ({
+ value: tz,
+ label: tz,
+ }));
+}
+
+// Helper: Generate week days and slots
+function getWeekDaysAndSlots(selectedTz) {
+ const weekDays = [];
+ const today = moment().tz(selectedTz);
+ for (let i = 0; i < 7; i++) {
+ const day = today.clone().add(i, "days");
+ weekDays.push({
+ label: day.format("dddd"),
+ date: day.format("MMMM D"),
+ dateISO: day.format("YYYY-MM-DD"),
+ slots: [
+ "9:00am",
+ "9:15am",
+ "9:30am",
+ "9:45am",
+ "10:00am",
+ "10:15am",
+ "10:30am",
+ "10:45am",
+ "11:00am",
+ "11:15am",
+ "11:30am",
+ "11:45am",
+ "12:00pm",
+ "12:15pm",
+ "12:30pm",
+ "12:45pm",
+ "1:00pm",
+ "1:15pm",
+ ],
+ });
+ }
+ return { weekDays, maxSlots: 17 };
+}
+
+// Timezone selection screen
+router.get("/timezone", function (req, res) {
+ res.render("timezone", {
+ timezones: getAllTimezones(),
+ timezoneGroups: getTimezoneGroups(),
+ });
+});
+
+// Calendar slot selection screen
+router.get("/calendar", function (req, res) {
+ const selectedTimezone = req.query.tz || "America/New_York";
+ const { weekDays, maxSlots } = getWeekDaysAndSlots(selectedTimezone);
+ res.render("calendar", {
+ selectedTimezone,
+ weekDays,
+ maxSlots,
+ showPrevWeek: false, // For demo, only next week link
+ });
+});
+
+// Booking form screen
+router.get("/book", function (req, res) {
+ const selectedTimezone = req.query.tz || "America/New_York";
+ res.render("booking-form", {
+ selectedTimezone,
+ });
+});
+
+// Booking POST (simulate success)
+router.post("/book", function (req, res) {
+ // Here you would save booking info to DB
+ res.redirect("/success");
+});
+
+// Success screen
+router.get("/success", function (req, res) {
+ res.render("success");
+});
+
+// Home page redirect to timezone selection
+router.get("/", function (req, res) {
+ res.redirect("/timezone");
});
module.exports = router;
diff --git a/day17/views/booking-form.ejs b/day17/views/booking-form.ejs
new file mode 100644
index 0000000..57087ef
--- /dev/null
+++ b/day17/views/booking-form.ejs
@@ -0,0 +1,56 @@
+<%- include('partials/header') %>
+
+
Pick a date and time
+
Duration: 1 hour
+
Your timezone: <%= selectedTimezone %>
+
+
+<%- include('partials/footer') %>
diff --git a/day17/views/calendar.ejs b/day17/views/calendar.ejs
new file mode 100644
index 0000000..20621f9
--- /dev/null
+++ b/day17/views/calendar.ejs
@@ -0,0 +1,58 @@
+<%- include('partials/header') %>
+
+
Pick a date and time
+
Duration: 1 hour
+
Your timezone: <%= selectedTimezone %> (Change)
+
+
+
+
+ <% weekDays.forEach(function(day) { %>
+ |
+
+ <%= day.label %>
+
+ <%= day.date %>
+ |
+ <% }) %>
+
+
+
+ <% for (let i = 0; i < maxSlots; i++) { %>
+
+ <% weekDays.forEach(function(day) { %>
+ |
+ <% if (day.slots[i]) { %>
+
+ <% } %>
+ |
+ <% }) %>
+
+ <% } %>
+
+
+
+
+
+<%- include('partials/footer') %>
diff --git a/day17/views/partials/footer.ejs b/day17/views/partials/footer.ejs
new file mode 100644
index 0000000..3ea674b
--- /dev/null
+++ b/day17/views/partials/footer.ejs
@@ -0,0 +1 @@
+
diff --git a/day17/views/partials/header.ejs b/day17/views/partials/header.ejs
new file mode 100644
index 0000000..f396e52
--- /dev/null
+++ b/day17/views/partials/header.ejs
@@ -0,0 +1,11 @@
+
+ Calendar
+
diff --git a/day17/views/success.ejs b/day17/views/success.ejs
new file mode 100644
index 0000000..bf70b11
--- /dev/null
+++ b/day17/views/success.ejs
@@ -0,0 +1,7 @@
+<%- include('partials/header') %>
+
+
+ Thanks for filling in the form. You will be emailed next steps.
+
+
+<%- include('partials/footer') %>
diff --git a/day17/views/timezone.ejs b/day17/views/timezone.ejs
new file mode 100644
index 0000000..4fe458c
--- /dev/null
+++ b/day17/views/timezone.ejs
@@ -0,0 +1,48 @@
+<%- include('partials/header') %>
+
+
Pick a date and time
+
Duration: 1 hour
+
+
+
+
+
+
TIME ZONE
+
+ <% Object.keys(timezoneGroups).forEach(function(region) { %>
+
+
<%= region %>
+ <% timezoneGroups[region].forEach(function(tz) { %>
+
+
+ <%= tz.label %>
+
+ <% }) %>
+
+ <% }) %>
+
+
+
+
+
+
+
+
+<%- include('partials/footer') %>