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 %>

+
+

Additional Information

+ + + + + + + + + + + +
+
+<%- 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) { %> + + <% }) %> + + + + <% for (let i = 0; i < maxSlots; i++) { %> + + <% weekDays.forEach(function(day) { %> + + <% }) %> + + <% } %> + +
+
+ <%= day.label %> +
+
<%= day.date %>
+
+ <% if (day.slots[i]) { %> +
+ + + +
+ <% } %> +
+
+
+ <% if (showPrevWeek) { %> + Previous Week + <% } %> + Next Week +
+
+<%- 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') %>