diff --git a/Exercise 1/css/style.css b/Exercise 1/css/style.css new file mode 100644 index 00000000..e69de29b diff --git a/Exercise 1/index.html b/Exercise 1/index.html new file mode 100644 index 00000000..0522ec28 --- /dev/null +++ b/Exercise 1/index.html @@ -0,0 +1,143 @@ + + + + + + Exercise 1 By Mohamad Dabbabo + + + + +

Exercise 1 by Mohamad Dabbabo

+

Date Submitted: 28/04/2025

+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
Some facts about me:
First NameLast NameFavorite MovieFavorite SongFavorite Video
MohamadDabbabo + +

Ghost in the Shell (1995)
Cyberpunk, AI, Philosophy, Identity

+ + Ghost in the Shell poster + +
+

Click “Play” to hear this concert performance
held in memory of the 2011 victims.

+ +
+ +
+ + +
+ + +

Enter your details below:

+ +
+ + +

+ + + +

+ + + +

+ + +
+ Available Days: +
+
+
+
+
+
+ +
+
+ + + +

+ + +
+ Working on a new website? +
+ +
+
+ + + +

+ + + + +
+ + + diff --git a/Exercise 1/js/script.js b/Exercise 1/js/script.js new file mode 100644 index 00000000..e69de29b diff --git a/Exercise 1/media/Kenji_Kawai_Making_of_a_Cyborg.mp3 b/Exercise 1/media/Kenji_Kawai_Making_of_a_Cyborg.mp3 new file mode 100644 index 00000000..f38739f7 Binary files /dev/null and b/Exercise 1/media/Kenji_Kawai_Making_of_a_Cyborg.mp3 differ diff --git a/Exercise 1/media/ai_biology_animation.mp4 b/Exercise 1/media/ai_biology_animation.mp4 new file mode 100644 index 00000000..1375f758 Binary files /dev/null and b/Exercise 1/media/ai_biology_animation.mp4 differ diff --git a/Exercise 2/css/style.css b/Exercise 2/css/style.css new file mode 100644 index 00000000..cf7bfd8e --- /dev/null +++ b/Exercise 2/css/style.css @@ -0,0 +1,127 @@ +/* palette variables (for later) */ +:root{ + --primary:#0077b6; /* deep blue */ + --accent:#00b4d8; /* cyan */ + --light:#f0f8ff; /* very light blue */ + --bg:#e8f4ff; /* page background */ + --border:#185b8c; /* darker blue */ + --table-head:#e63946;/* red */ + } + + /* base */ + body{ + margin:0; + font-family:Arial, Helvetica, sans-serif; /* Fonts I picked that resonate with me */ + background:var(--bg); + color:#222; + padding:0 12px 40px; + } + + /* reusable soft-card */ + .soft{ + background:#fff; + border:2px solid var(--border); + padding:20px; + margin:24px auto; + max-width:940px; + box-shadow:0 0 12px #0077b620; /* subtle bloom */ + } + + /* headings */ + h1{ + color:#fff; + background:var(--primary); + text-align:center; + margin:0 -12px 24px; + padding:28px 12px; + text-shadow:0 0 6px #ffffffaa; /* glowing effect */ + } + h2{ + color:var(--primary); + border-bottom:4px solid var(--accent); + padding-bottom:4px; + margin:28px auto 12px; + max-width:940px; + text-shadow:0 0 4px #00b4d880; + } + h3{ + margin:6px 0 24px; + color:var(--accent); + text-align:center; + text-shadow:0 0 4px #00b4d880; + } + + /* description paragraph */ + p:first-of-type{ + max-width:760px; + margin:0 auto 24px; + } + + /* table */ + table { + border-collapse: collapse; + width: 50%; + margin-left: auto; + margin-right: auto; + text-align: center; + } + + th,td{ + border:1px solid var(--accent); + padding:10px 6px; + text-align:center; + } + th{ + background:var(--table-head); + color:#fff; + } + + /* media thumbs */ + img{ + border:4px solid var(--accent); + padding:4px; + background:#fff; + box-shadow:0 0 8px #00b4d820; + } + + /* form */ + form{ + background:#fff; + border:2px solid var(--border); + padding:20px; + max-width:540px; + margin:24px auto; + box-shadow:0 0 10px #0077b620; + } + label{display:block;margin:10px 0;} + input,select,textarea{ + width:100%;max-width:100%; + padding:6px;border:1px solid #ccc;margin-top:4px; + } + fieldset{ + border:1px solid var(--accent); + padding:10px;margin:14px 0; + } + + /* Button CSS, background uses primary variable, no border, changes cursor to pointer */ + button{ + background:var(--primary); + color:#fff; + border:none; + padding:8px 18px; + margin-top:12px; + cursor:pointer; + } + /* This makes the reset button have a unique */ + button[type="reset"]{background:var(--accent);} + + /* This uses :hover to change colors */ + button:hover{ + background:var(--light); + color:#000000; + box-shadow:0 0 6px #00b4d8aa; + } + + /* figures */ + figure{margin:24px auto;max-width:320px;text-align:center;} + figcaption{margin-bottom:6px;font-weight:bold;} \ No newline at end of file diff --git a/Exercise 2/index.html b/Exercise 2/index.html new file mode 100644 index 00000000..5d2943ab --- /dev/null +++ b/Exercise 2/index.html @@ -0,0 +1,114 @@ + + + + + + Robotics & AI Expo 2025 – Information Page + + + + + + + + +

Robotics & AI Expo 2025

+

Saturday · 24 May 2025 · Gesamtschule Konradsdorf · 10:00–17:00

+ + +

+ Ever wanted to unleash your AI skills? Join us for a day of hands-on workshops (practical), thrilling competitions (hackathons), and thought-provoking debates (About future AI)! + You will learn about the latest in robotics and AI, and have the chance to showcase your own projects. Even bring your arguments to the table! + So what are you waiting for? Come join the event! +

+ + +

Event Schedule

+ + + + + + + + + + + + + + + +
TimeActivityLocation
10 : 00Opening KeynoteAula
11 : 00Autonomous-Bot RaceSporthalle
13 : 00ML Workshop: "Teach a CNN to lower grade students"Lab 1B
15 : 30Debate – "Will AI surpass us?"Auditorium
16 : 45Awards & ClosingAula
+ + +

Sneak-peek

+

+ + Expo poster by GPT-4o image gen + +

+ + + + +
+
10-seconds highlight reel:
+ +
+ + + +

Register / Contact Us

+
+ + + + + + + + + +
+ Interested in: + + + +
+ + + + + +
+ + + + + diff --git a/Exercise 2/media/matplotlib_animation.ipynb b/Exercise 2/media/matplotlib_animation.ipynb new file mode 100644 index 00000000..fa6f9f50 --- /dev/null +++ b/Exercise 2/media/matplotlib_animation.ipynb @@ -0,0 +1,208 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 10, + "id": "0930f850", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABOwAAAEcCAYAAAB07ys7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAA9hAAAPYQGoP6dpAABTS0lEQVR4nO3dd3gU1eLG8Xc32Wx6AVIpCYQO0qULSBGUqmABlWJHVBDsWOCKqBfxYgOx4k9BVATEBoKAFOlKL6FDgCQkIb1n5/fHyoa9CRAUzVz9fp5nH8jMmZkzs7uzu++cc8ZiGIYhAAAAAAAAAKZgregKAAAAAAAAAChBYAcAAAAAAACYCIEdAAAAAAAAYCIEdgAAAAAAAICJENgBAAAAAAAAJkJgBwAAAAAAAJgIgR0AAAAAAABgIgR2AAAAAAAAgIkQ2AEAAAAAAAAmQmAHAMDfyIQJE2SxWJScnPynb2vlypWyWCxauXLln74twKyOHDkii8WiV1555U/f1qxZs2SxWHTkyJFLXpb3KwAA/1sI7AAAqCBnf3yffXh6eqpq1aoaPny4Tpw4UdHVc5k+fbpmzZpV0dVw+eijj9S4cWP5+vqqevXqGjp0qE6ePPm71jV9+nRZLBa1adPmvGUsFoseeOCBi64rJibG7fk899GrV6/fVb8/m8Ph0KxZs9SvXz9Vr15dfn5+aty4sSZNmqS8vLwyl3n//ffVoEEDeXt7q06dOnrjjTdKlZk/f75uvvlm1apVS76+vqpXr57GjRuntLS0UmXPd9zuu+++y727br7++mt17txZYWFh8vX1Va1atXTTTTdp8eLFf+p2AQAAysOzoisAAMA/3b/+9S/VrFlTeXl5Wr9+vWbNmqU1a9Zo586d8vb2rujqafr06apSpYqGDx/uNr1Tp07Kzc2Vl5fXX1aXBQsWaPjw4ercubMeeOABJSUlad68eYqLi1NUVNQlr2/27NmKiYnRxo0bdeDAAdWuXfsP1a9Zs2YaN25cqem/p25/hZycHI0YMUJt27bVfffdp7CwMK1bt07PPfecfvzxRy1fvlwWi8VVfubMmbrvvvs0cOBAjR07VqtXr9ZDDz2knJwcPf74465y99xzj6KionTbbbepRo0a2rFjh95880199913+uWXX+Tj4+NWj7KOW926df+0/X7llVf06KOPqnPnznryySfl6+urAwcOaNmyZZo7d65pA1YAAPDPQWAHAEAFu/baa9WqVStJ0l133aUqVaro5Zdf1qJFi3TTTTdVcO3Oz2q1/uWB4ty5c1WpUiUtXrzYte1nn31WBQUFl7yuw4cP6+eff9b8+fN17733avbs2Xruuef+UP2qVq2q22677Q+t46/k5eWltWvXqn379q5pd999t2JiYlyhXffu3SVJubm5Gj9+vHr37q158+a5yjocDj3//PO65557FBISIkmaN2+eunTp4ratli1batiwYZo9e7buuusut3l/5XErKirS888/rx49euiHH34oNT8pKekvqQcAAMCF0CUWAACTueqqqyRJBw8edJu+fPlyXXXVVfLz81NwcLD69++vPXv2lLmO5ORk3XTTTQoMDFTlypU1evToUl0czwYXsbGxstvtiomJ0VNPPaX8/HxXmZiYGO3atUs//fSTq6vi2SDmfGNibdiwQdddd51CQkLk5+enJk2a6LXXXnPNT0hI0IgRI1StWjXZ7XZFRkaqf//+5RqXy2q1qqioSB4eHm7Tf08rv9mzZyskJES9e/fWoEGDNHv27Etex6VKSkpSaGiounTpIsMwXNMPHDggPz8/3Xzzza5pXbp0UePGjbVlyxa1b99ePj4+qlmzpt5+++0y13vnnXcqPDxc3t7eatq0qT766KOL1sfLy8strDvr+uuvlyS319eKFSuUkpKi+++/363sqFGjlJ2drW+//dat7uVZ57kKCgqUnZ190Tr/UcnJycrIyFCHDh3KnB8WFub2d15eniZMmKC6devK29tbkZGRuuGGG0q9PyXpnXfecb2frrzySm3atKlUmb1792rQoEGqVKmSvL291apVKy1atKhUuV27dqlr167y8fFRtWrVNGnSJDkcjlLlLBaLJkyYUGp6TExMqVaxZdmwYYN69eqloKAg+fr6qnPnzlq7du1FlwMAAH8uAjsAAEzmbHB1trWSJC1btkw9e/ZUUlKSJkyYoLFjx+rnn39Whw4dygy6brrpJuXl5enFF1/Uddddp9dff1333HOPW5m77rpLzz77rFq0aKH//Oc/6ty5s1588UXdcsstrjLTpk1TtWrVVL9+fX388cf6+OOPNX78+PPWfenSperUqZN2796t0aNHa+rUqbr66qv1zTffuMoMHDhQCxYs0IgRIzR9+nQ99NBDyszM1LFjxy56bEaMGKGMjAw9++yzFy17MbNnz9YNN9wgLy8vDR48WPv37y8zYLkUhYWFSk5OLvXIzc2V5AyDZsyYoZ9++sk19pvD4dDw4cMVEBCg6dOnu63vzJkzuu6669SyZUv9+9//VrVq1TRy5Eh98MEHrjK5ubnq0qWLPv74Y916662aMmWKgoKCNHz4cLeg9FIkJCRIkqpUqeKa9uuvv0qSqzXoWS1btpTVanXNv5R1nrV8+XL5+vrK399fMTExv7ve5REWFiYfHx99/fXXSk1NvWDZ4uJi9enTRxMnTlTLli01depUjR49Wunp6dq5c6db2Tlz5mjKlCm69957NWnSJB05ckQ33HCDCgsLXWV27dqltm3bas+ePXriiSc0depU+fn5acCAAVqwYIGrXEJCgq6++mpt3bpVTzzxhMaMGaP/+7//u+zHZfny5erUqZMyMjL03HPPafLkyUpLS1PXrl21cePGy7otAABwiQwAAFAhPvzwQ0OSsWzZMuP06dPG8ePHjXnz5hmhoaGG3W43jh8/7irbrFkzIywszEhJSXFN27Ztm2G1Wo2hQ4e6pj333HOGJKNfv35u27r//vsNSca2bdsMwzCMrVu3GpKMu+66y63cI488Ykgyli9f7prWqFEjo3PnzqXqv2LFCkOSsWLFCsMwDKOoqMioWbOmER0dbZw5c8atrMPhMAzDMM6cOWNIMqZMmVL+A3WO6dOnG3a73ZBkvPbaa79rHYZhGJs3bzYkGUuXLnXVr1q1asbo0aNLlZVkjBo16qLrjI6ONiSV+XjxxRfdyg4ePNjw9fU14uLijClTphiSjIULF7qV6dy5syHJmDp1qmtafn6+67VQUFBgGIZhTJs2zZBkfPLJJ65yBQUFRrt27Qx/f38jIyOj3MflrO7duxuBgYFuz+OoUaMMDw+PMsuHhoYat9xyywXXeeeddxoeHh5GXFyc2/S+ffsaL7/8srFw4ULj/fffN6666ipDkvHYY49dcr3L69lnnzUkGX5+fsa1115rvPDCC8aWLVtKlfvggw8MScarr75aat7Z1/Thw4cNSUblypWN1NRU1/yvvvrKkGR8/fXXrmndunUzrrjiCiMvL89tPe3btzfq1KnjmjZmzBhDkrFhwwbXtKSkJCMoKMiQZBw+fNg1XZLx3HPPlapfdHS0MWzYMNff//1+dTgcRp06dYyePXu69sUwDCMnJ8eoWbOm0aNHjzKOHAAA+KvQwg4AgArWvXt3hYaGqnr16ho0aJD8/Py0aNEiVatWTZJ06tQpbd26VcOHD1elSpVcyzVp0kQ9evTQd999V2qdo0aNcvv7wQcflCRX2bP/jh071q3c2YH/z+3eWF6//vqrDh8+rDFjxig4ONht3tkbF/j4+MjLy0srV67UmTNnLmn9X331lUaNGqV58+Zp/PjxGjNmjD788EO3MvXq1dPtt99+0XXNnj1b4eHhuvrqq131u/nmmzV37lwVFxdfUr3O1aZNGy1durTUY/DgwW7l3nzzTQUFBWnQoEF65plndPvtt6t///6l1ufp6al7773X9beXl5fuvfdeJSUlacuWLZKcz2VERITbNmw2mx566CFlZWXpp59+uqR9mDx5spYtW6aXXnrJ7Xm80A1GvL29Xa0IyzJnzhy9//77GjdunOrUqeM2b9GiRXrsscfUv39/3XHHHfrpp5/Us2dPvfrqq4qPj7+kupfXxIkTNWfOHDVv3lxLlizR+PHj1bJlS7Vo0cKty+6XX36pKlWquN4/5zr3ZhySdPPNN7u1ij3btf3QoUOSpNTUVC1fvlw33XSTMjMzXa0vU1JS1LNnT+3fv991d+jvvvtObdu2VevWrV3rCw0N1a233nrZjsHWrVu1f/9+DRkyRCkpKa76ZGdnq1u3blq1alWZXXABAMBfg5tOAABQwd566y3VrVtX6enp+uCDD7Rq1SrZ7XbX/KNHj0pyhlH/rUGDBlqyZImys7Pl5+fnmv7foUhsbKysVqur++zRo0dltVpL3RU1IiJCwcHBrm1eirNjejVu3Pi8Zex2u15++WWNGzdO4eHhatu2rfr06aOhQ4cqIiLigut//PHHde2116pPnz7q06ePEhMTdffddysgIECDBg1STk6ODh8+XGa4cq7i4mLNnTtXV199tQ4fPuya3qZNG02dOlU//vijrrnmmkvY8xJVqlRx3aThQipVqqTXX39dN954o8LDw/X666+XWS4qKsrteZVK7p565MgRtW3bVkePHlWdOnVktbpfh23QoIEkXdJz+dlnn+npp5/WnXfeqZEjR7rN8/HxOe/NPfLy8krd+fWs1atX684771TPnj31wgsvXLQOFotFDz/8sJYsWaKVK1ee92YUxcXFOn36dJnzfHx8FBQUdMHtDB48WIMHD1ZGRoY2bNigWbNmac6cOerbt6/rDs0HDx5UvXr15Ol58a/MNWrUcPv7bHh3Npg+cOCADMPQM888o2eeeabMdSQlJalq1ao6evSo2rRpU2p+WeeA32v//v2SpGHDhp23THp6ulsICQAA/joEdgAAVLDWrVu7xgUbMGCAOnbsqCFDhmjfvn3y9/e/LNv479ZAF5v+ZxozZoz69u2rhQsXasmSJXrmmWf04osvavny5WrevHmZy6Smpmrfvn1uLYzefvttnT59WkOGDJGfn58OHTokq9WqQYMGXXD7y5cv16lTpzR37lzNnTu31PzZs2f/7sDuUixZskSSM9CJj48v1Srxr7Z06VINHTpUvXv3LvPGFpGRkSouLlZSUpLbjRkKCgqUkpKiqKioUsts27ZN/fr1U+PGjTVv3rxyBV+SVL16dUm64Bhzx48fV82aNcucN2zYMM2aNatc2woMDFSPHj3Uo0cP2Ww2ffTRR9qwYYM6d+5cruXP+u8boZxl/HZzkbOt1R555BH17NmzzLL/HaD/ERdrKXq2PlOmTFGzZs3KLHO5zj8AAODSEdgBAGAiHh4eevHFF3X11VfrzTff1BNPPKHo6GhJ0r59+0qV37t3r6pUqVKqFdb+/fvdwowDBw7I4XAoJiZGkhQdHS2Hw6H9+/e7WmJJUmJiotLS0lzblMof6sXGxkqSdu7cedFWZrGxsRo3bpzGjRun/fv3q1mzZpo6dao++eSTMsufrcPx48dd0zw8PDR37lxdc801GjhwoAIDAzVy5MiLttSbPXu2wsLC9NZbb5WaN3/+fC1YsEBvv/32eVuMXQ6LFy/We++9p8cee0yzZ8/WsGHDtGHDhlKB1smTJ0u1noyLi5Mkt+dy+/btcjgcbq3s9u7d65p/MRs2bND111+vVq1a6fPPPy8zWDsb6mzevFnXXXeda/rmzZvlcDhKhT4HDx5Ur169FBYWpu++++6Swp+z3UhDQ0PPWyYiIkJLly4tc15Z4WF5tGrVSh999JFOnTolyfk63bBhgwoLC2Wz2X7XOs+qVauWJGd35Yu9P6Kjo10t4M5V1jkgJCREaWlpbtMKCgpc+3A+Z9+vgYGB5WoVCgAA/lqMYQcAgMl06dJFrVu31rRp05SXl6fIyEg1a9ZMH330kdsP8507d+qHH35wC0/O+u8w6uwdSa+99lpJci0zbdo0t3KvvvqqJKl3796uaX5+fqUCgbK0aNFCNWvW1LRp00qVP9vKKCcnR3l5eW7zYmNjFRAQoPz8/POuOyQkRC1atNCcOXNcQZTkHDvt448/lsPhUGJiogYMGHDBOubm5mr+/Pnq06ePBg0aVOrxwAMPKDMzU4sWLbro/v5eaWlpuuuuu9S6dWtNnjxZ7733nn755RdNnjy5VNmioiLNnDnT9XdBQYFmzpyp0NBQtWzZUpLzuUxISNBnn33mttwbb7whf3//i7YU27Nnj3r37q2YmBh988035w0qu3btqkqVKmnGjBlu02fMmCFfX1+310xCQoKuueYaWa1WLVmy5LzBW2pqaqmWYIWFhXrppZfk5eXlGmOwLN7e3urevXuZj4YNG553uZycHK1bt67Med9//72kkq6nAwcOVHJyst58881SZc++pssrLCxMXbp00cyZM8sM087t3nvddddp/fr1bndqPX36tGbPnl1qudjYWK1atcpt2jvvvHPRFnYtW7ZUbGysXnnlFWVlZV2wPgAA4K9HCzsAAEzo0Ucf1Y033qhZs2bpvvvu05QpU3TttdeqXbt2uvPOO5Wbm6s33nhDQUFBmjBhQqnlDx8+rH79+qlXr15at26dPvnkEw0ZMkRNmzaVJDVt2lTDhg3TO++8o7S0NHXu3FkbN27URx99pAEDBrgFJS1bttSMGTM0adIk1a5dW2FhYeratWupbVqtVs2YMUN9+/ZVs2bNNGLECEVGRmrv3r3atWuXlixZori4OHXr1k033XSTGjZsKE9PTy1YsECJiYm65ZZbLnhM3njjDXXv3l2tW7fWvffeq/r16+vIkSP64IMPFB4eLqvVqiFDhmjDhg2uG3b8t0WLFikzM1P9+vUrc37btm0VGhqq2bNn6+abb75gfcpy4sSJMlsJ+vv7u8LE0aNHKyUlRcuWLZOHh4d69eqlu+66S5MmTVL//v1dz5HkbCn28ssv68iRI6pbt64+++wzbd26Ve+8846rxdc999yjmTNnavjw4dqyZYtiYmI0b948rV27VtOmTVNAQMB565uZmamePXvqzJkzevTRR0vdbCQ2Nlbt2rWT5BwX7vnnn9eoUaN04403qmfPnlq9erU++eQTvfDCC243ROnVq5cOHTqkxx57TGvWrNGaNWtc88LDw9WjRw9Jzudj0qRJGjRokGrWrKnU1FTN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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# This code was designed for the animation, since I didn't want to spend too much time doing it in Blender, I decided a quick Matplotlib would do it.\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.animation as anim\n", + "from matplotlib.patches import Rectangle\n", + "from IPython.display import HTML\n", + "import numpy as np\n", + "from matplotlib.animation import FFMpegWriter\n", + "import matplotlib as mpl\n", + "import os\n", + "import imageio_ffmpeg\n", + "\n", + "# --- DATA ---\n", + "events = [\n", + " (\"Opening Keynote\", 10.00, 11.00),\n", + " (\"Autonomous-Bot Race\", 11.00, 13.00),\n", + " (\"ML Workshop – “Teach a CNN”\", 13.00, 15.50),\n", + " (\"Debate – “Will AI surpass us?”\",15.50, 16.75),\n", + " (\"Awards & Closing\", 16.75, 17.00),\n", + "]\n", + "\n", + "# make sure the folder exists\n", + "out_dir = r'C:\\Users\\iHunter\\OneDrive\\Desktop\\Building-U\\bu-learning\\Exercise 2\\media'\n", + "os.makedirs(out_dir, exist_ok=True)\n", + "out_path = os.path.join(out_dir, 'robotics_schedule.mp4')\n", + "\n", + "# --- FIGURE SETUP ---\n", + "fig, ax = plt.subplots(figsize=(16, 2.8))\n", + "mpl.rcParams['animation.ffmpeg_path'] = imageio_ffmpeg.get_ffmpeg_exe()\n", + "\n", + "# hide everything (axes, ticks, spines)\n", + "ax.axis('off')\n", + "\n", + "\n", + "# Let's assume 1 hour between each event\n", + "margin = 1.0\n", + "ax.set_xlim(9.8, events[-1][2] + margin)\n", + "ax.set_ylim(-0.05, 0.75)\n", + "\n", + "# horizontal baseline\n", + "ax.hlines(0.05, events[0][1], events[-1][2] + margin, lw=1.5, color=\"k\")\n", + "\n", + "ax.set_title(\"Robotics & AI Expo 2025 – Schedule\", pad=25)\n", + "\n", + "# color palette + contrast helper\n", + "palette = plt.get_cmap(\"tab10\").colors\n", + "\n", + "\n", + "def best_text_colour(rgb):\n", + " r, g, b = rgb\n", + " return \"black\" if 0.2126*r + 0.7152*g + 0.0722*b > 0.55 else \"white\"\n", + "\n", + "# Set minimum bar width (for display purposes)\n", + "MIN_BAR_WIDTH = 0.9\n", + "\n", + "bars, labels, grads = [], [], [None]*len(events)\n", + "\n", + "# initial patch + text creation (width=0 to animate in)\n", + "for i, (lab, start, end) in enumerate(events):\n", + " real_w = end - start\n", + " disp_w = max(real_w, MIN_BAR_WIDTH) # enforce minimum for display\n", + " color = palette[i % len(palette)]\n", + " y0, h = 0.20, 0.40\n", + "\n", + " # bar (starts at zero, grows to disp_w)\n", + " rect = Rectangle((start, y0), 0, h, color=color, ec=\"none\", zorder=1)\n", + " ax.add_patch(rect)\n", + " bars.append((rect, real_w, disp_w))\n", + "\n", + " # label (alpha=0 at start)\n", + " txt = ax.text(start, y0 + h/2, lab,\n", + " ha=\"left\", va=\"center\",\n", + " fontsize=9, alpha=0.0,\n", + " color=best_text_colour(color),\n", + " zorder=2)\n", + " labels.append(txt)\n", + "\n", + "# gradient helper function\n", + "def make_gradient(ax, x0, w, col, y0=0.20, h=0.40, n=120):\n", + " try:\n", + " grad = np.linspace(0,1,n)[:,None] * np.array(col)\n", + " im = ax.imshow(grad[None,:,:],\n", + " extent=[x0, x0+w, y0, y0+h],\n", + " aspect=\"auto\", zorder=0.5)\n", + " im.set_clip_path(Rectangle((x0, y0), w, h, transform=ax.transData))\n", + " return im\n", + " except:\n", + " return None\n", + "\n", + "# --- ANIMATION LOGIC ---\n", + "fps = 25\n", + "frames_per_event = 40\n", + "pause_frames = 10\n", + "total_frames = len(events) * (frames_per_event + pause_frames)\n", + "\n", + "def animate(frame):\n", + " idx, local = divmod(frame, frames_per_event + pause_frames)\n", + " artists = []\n", + "\n", + " for i, ((rect, real_w, disp_w), txt) in enumerate(zip(bars, labels)):\n", + " s = events[i][1]\n", + " # Past events → full width\n", + " if i < idx:\n", + " rect.set_x(s)\n", + " rect.set_width(disp_w)\n", + " rect.set_edgecolor(\"k\")\n", + " txt.set_alpha(1.0)\n", + "\n", + " # Current event sliding in\n", + " elif i == idx and local < frames_per_event:\n", + " p = local / frames_per_event\n", + " rect.set_x(s)\n", + " rect.set_width(disp_w * p)\n", + " # if the _real_ bar is tall enough, fade in text; otherwise show full alpha\n", + " if real_w >= MIN_BAR_WIDTH:\n", + " txt.set_alpha(p)\n", + " else:\n", + " txt.set_alpha(1.0)\n", + "\n", + " # Landed/current-pause (blink outline)\n", + " elif i == idx:\n", + " rect.set_x(s)\n", + " rect.set_width(disp_w)\n", + " rect.set_edgecolor(\"k\" if (local - frames_per_event) % 2 else \"none\")\n", + " txt.set_alpha(1.0)\n", + "\n", + " # Future events → hidden\n", + " else:\n", + " rect.set_width(0)\n", + " txt.set_alpha(0.0)\n", + "\n", + " # label placement (center if wide, outside if skinny)\n", + " if disp_w >= MIN_BAR_WIDTH:\n", + " txt.set_x(s + disp_w/2)\n", + " txt.set_ha(\"center\")\n", + " else:\n", + " txt.set_x(s + disp_w + 0.05)\n", + " txt.set_ha(\"left\")\n", + "\n", + " artists += [rect, txt]\n", + "\n", + " return artists\n", + "\n", + "ani = anim.FuncAnimation(fig, animate,\n", + " frames=total_frames,\n", + " interval=1000/fps,\n", + " blit=True)\n", + "\n", + "# Display in Jupyter/VS Code interactive window\n", + "HTML(ani.to_jshtml())\n", + "# save to media/ folder (make sure it exists!)\n", + "\n", + "# --- ANIMATION SAVE ---\n", + "# set up the writer\n", + "writer = FFMpegWriter(\n", + " fps=fps,\n", + " codec='libx264',\n", + " bitrate=1800,\n", + " extra_args=['-pix_fmt', 'yuv420p']\n", + ")\n", + "\n", + "# save the file\n", + "ani.save(out_path, writer=writer)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Exercise 2/media/poster.png b/Exercise 2/media/poster.png new file mode 100644 index 00000000..d47417bd Binary files /dev/null and b/Exercise 2/media/poster.png differ diff --git a/Exercise 2/media/robotics_schedule.mp4 b/Exercise 2/media/robotics_schedule.mp4 new file mode 100644 index 00000000..57dd3454 Binary files /dev/null and b/Exercise 2/media/robotics_schedule.mp4 differ diff --git a/Exercise 3/index.html b/Exercise 3/index.html new file mode 100644 index 00000000..70a4683b --- /dev/null +++ b/Exercise 3/index.html @@ -0,0 +1,121 @@ + + + + + + + Exercise 3 – Advanced CSS Demo + + + + + + + +
+
First Name
+
Last Name
+
Favorite Movie
+
Favorite Song
+
Favorite Video
+ +
Mohamad
+
Dabbabo
+ +
+ +
+
+ +
+
+ + + + +
+
First Name
+
Mohamad
+ +
Last Name
+
Dabbabo
+ +
Favorite Movie
+ + +
Favorite Song
+
+ +
+ +
Favorite Video
+
+ +
+
+ + + +
+

Enter your details below:

+
+ + + + +
+ Available Meeting Days: +
+ + + + + + + +
+
+ + + +
+ Currently working on a new website? + + +
+ + + +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/Exercise 3/media/background-desktop.jpg b/Exercise 3/media/background-desktop.jpg new file mode 100644 index 00000000..22ffcf85 Binary files /dev/null and b/Exercise 3/media/background-desktop.jpg differ diff --git a/Exercise 3/media/background-mobile.jpg b/Exercise 3/media/background-mobile.jpg new file mode 100644 index 00000000..e2c914f9 Binary files /dev/null and b/Exercise 3/media/background-mobile.jpg differ diff --git a/Exercise 3/media/marvel_studios.mp4 b/Exercise 3/media/marvel_studios.mp4 new file mode 100644 index 00000000..bcdbaa6b Binary files /dev/null and b/Exercise 3/media/marvel_studios.mp4 differ diff --git a/Exercise 3/media/rick_roll.mp3 b/Exercise 3/media/rick_roll.mp3 new file mode 100644 index 00000000..89f4bfd9 Binary files /dev/null and b/Exercise 3/media/rick_roll.mp3 differ diff --git a/Exercise 3/style.css b/Exercise 3/style.css new file mode 100644 index 00000000..e9e795bd --- /dev/null +++ b/Exercise 3/style.css @@ -0,0 +1,178 @@ +/* ---------- Root + reset ---------- */ +*{box-sizing:border-box;margin:0;padding:0;} + +:root{ + --bg-dark:#0b1522; + --bg-accent:#111a2e; + --grid-border:#8a8fa3; + --panel-bg:#24323fbb; + + --btn-green:#42b067; + --btn-green-hi:#c3ffd7; + --btn-red:#c84848; + --btn-red-hi:#ff9e9e; + + --text-light:#e6e6e6; + --highlight:#ffdc64; +} + +body{ + font-family:system-ui,Segoe UI,Roboto,sans-serif; + color:var(--text-light); + background:url("media/background-desktop.jpg") center/cover fixed no-repeat; + padding:1rem; + line-height:1.5; +} + +/* ---------- Helpers ---------- */ +.visually-hidden{ + position:absolute!important; + clip:rect(1px,1px,1px,1px); + width:1px;height:1px;overflow:hidden; +} + +/* ---------- Layout sections ---------- */ +.page-header{ + text-align:center; + border-bottom:3px solid var(--text-light); + margin-bottom:1rem; +} +.page-header .highlight{color:var(--highlight);} +.page-header .sub{font-size:.8rem;opacity:.9;} + +.content-section{max-width:1280px;margin:2rem auto 0;} + +/* ---------- Fun-facts grids ---------- */ +.facts-grid{ + display:grid; + border:3px solid var(--grid-border); + background:var(--panel-bg); +} +.facts-grid .cell{ + padding:.75rem; + text-align:center; + border:2px solid var(--grid-border); +} +.facts-grid .head{background:var(--grid-border);font-weight:600;} +.facts-grid img{max-width:100%;height:auto;display:block;margin:0.5rem auto;} + +.cell.movie p{color:var(--highlight);font-style:italic;} +/* zebra accent */ +.facts-grid .cell:nth-child(10n+6), +.facts-grid .cell:nth-child(10n+7){background:#3a4857;} + +/* ---------- Form ---------- */ +.details-form{ + background:var(--panel-bg); + border:3px solid var(--grid-border); + padding:2rem 2.5rem; + margin:auto; + max-width:320px; + display:flex;flex-direction:column;gap:1rem; +} +.details-form label{display:flex;flex-direction:column;font-size:.9rem;gap:.25rem;} + +.details-form input:not([type=checkbox]):not([type=radio]), +.details-form select, +.details-form textarea{ + padding:.5rem; + border:1px solid #aaa; + border-radius:4px; +} +.details-form input:not([type=checkbox]):not([type=radio]):focus, +.details-form select:focus, +.details-form textarea:focus{outline:2px solid var(--highlight);} + + +/* remove default fieldset styles */ +.details-form fieldset{ + border:none; /* remove default ugly border */ + padding:0; + margin:0; + display:flex; + flex-direction:column; + gap:.5rem; +} + +/* radio button styles */ +.details-form fieldset label { + display: flex; /* for radio buttons */ + align-items: center; + gap: 0.5rem; /* space between radio and text */ + font-size: 0.9rem; + cursor: pointer; +} + +/* radio styles */ +.details-form input[type="radio"] { + accent-color: var(--highlight); + transform: scale(1.2); + cursor: pointer; +} + +.check-group{ + display:grid; + grid-template-columns:repeat(2,1fr); + gap:.5rem 1rem; + font-size:.9rem; + justify-items:center; + align-items:center; +} +.check-group label{display:flex;align-items:center;gap:.5rem;} + +/* buttons */ +.btn-row{display:flex;gap:.75rem;justify-content:center;} + +.btn{ + cursor:pointer;border:none; + padding:.5rem 1.5rem;border-radius:4px; + font-weight:600;color:#fff; + transition:transform .2s ease,background-color .2s ease; +} +.btn.submit{background:var(--btn-green);} +.btn.reset {background:var(--btn-red);} +.btn.submit:hover{ + transform:translateY(-2px); + background:var(--btn-green-hi); + color:var(--bg-dark); +} +.btn.reset:hover{ + transform:translateY(-2px); + background:var(--btn-red-hi); + color:var(--bg-dark); +} + +/* ---------- Footer ---------- */ +h2{text-align:center;} +.page-footer{margin-top:3rem;text-align:center;padding:.5rem;background:#800080aa;} +.page-footer .copy{font-size:.9rem;} + +/* ---------- Media queries ---------- */ + +/* Mobile <= 767 px */ +@media (max-width:767px){ + body{background:url("media/background-mobile.jpg") center/cover fixed no-repeat;} + + /* show mobile grid / hide desktop */ + .facts-grid-desktop{display:none;} + .facts-grid-mobile{ + grid-template-columns:1fr; + } + + .page-header h1{font-size:1.3rem;} + .details-form{max-width:100%;} +} + +/* Desktop >= 768 px */ +@media (min-width:768px){ + /* show desktop grid / hide mobile */ + .facts-grid-mobile{display:none;} + .facts-grid-desktop { + grid-template-columns: repeat(5, 1fr); + } + + .details-form{max-width:380px;} + + /* center poster a bit nicer on wide screens */ + .facts-grid-desktop .cell.movie img{width:240px;} +} diff --git a/Exercise 4/index.html b/Exercise 4/index.html new file mode 100644 index 00000000..0f6da6d0 --- /dev/null +++ b/Exercise 4/index.html @@ -0,0 +1,377 @@ + + + + + + + BuildingU Portfolio – Exercise 4 + + + + + + + + + + + + + + +
+
+

Welcome to My BuildingU Projects Page

+

Discover my work at Building-U.

+ View My Work +
+
+ View About Me +
+
+ + +
+
+

About Me

+ +
+
+ + Profile +
+ +
+

+ Hey, I'm Mohamad Dabbabo, a web‑dev and AI enthusiast on a quest to fuse code with creativity. + Lately I've been trying Bootstrap, which is perfect for rapid, responsive builds + (and great for this internship’s mini‑projects). +

+
+
+ HTML +
+
+ CSS +
+
+ Python +
+
+ C# +
+
+ LaTeX +
+
+ JavaScript +
+
+
+
+
+ + +
+
+

My Portfolio

+ +
+ +
+
+ HTML Project +
+
Project 1: HTML
+

+ A simple HTML page showcasing a table of personal favorites and a styled user input form using pure HTML. +

+ +
+
+
+ + +
+
+ CSS Project +
+
Project 2: CSS
+

+ A custom-styled event schedule for the Robotics & AI Expo 2025, featuring colored headers and responsive table layout powered by CSS. +

+ +
+
+
+ + +
+
+ Advanced CSS Project +
+
Project 3: Advanced CSS
+

+ A dark, AI-themed responsive redesign: hexagon background, grid-based layout, modals, and custom form styling with advanced CSS techniques. +

+ +
+
+
+ + +
+
+ Bootstrap Project +
+
Project 4: Bootstrap
+

+ A playful nod to the power of Bootstrap itself. +

+ +
+
+
+
+ + +
+
+

Contact Me

+ +
+
+ + +
Please enter your name.
+
+ +
+ + +
Please enter a valid email.
+
+ +
+ + +
Message can’t be empty.
+
+ +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Exercise 4/media/Bootstrap_logo.svg.png b/Exercise 4/media/Bootstrap_logo.svg.png new file mode 100644 index 00000000..8e9bb9d4 Binary files /dev/null and b/Exercise 4/media/Bootstrap_logo.svg.png differ diff --git a/Exercise 4/media/CSS3_logo_and_wordmark.svg.png b/Exercise 4/media/CSS3_logo_and_wordmark.svg.png new file mode 100644 index 00000000..25d65668 Binary files /dev/null and b/Exercise 4/media/CSS3_logo_and_wordmark.svg.png differ diff --git a/Exercise 4/media/Gemini_Generated_Image_mi342zmi342zmi34.jpg b/Exercise 4/media/Gemini_Generated_Image_mi342zmi342zmi34.jpg new file mode 100644 index 00000000..93e91ed8 Binary files /dev/null and b/Exercise 4/media/Gemini_Generated_Image_mi342zmi342zmi34.jpg differ diff --git a/Exercise 4/media/HTML5_logo_and_wordmark.svg.png b/Exercise 4/media/HTML5_logo_and_wordmark.svg.png new file mode 100644 index 00000000..3e8bbd2c Binary files /dev/null and b/Exercise 4/media/HTML5_logo_and_wordmark.svg.png differ diff --git a/Exercise 4/media/JavaScript-Logo.png b/Exercise 4/media/JavaScript-Logo.png new file mode 100644 index 00000000..8c6bd354 Binary files /dev/null and b/Exercise 4/media/JavaScript-Logo.png differ diff --git a/Exercise 4/media/cropped-BUlogo.png b/Exercise 4/media/cropped-BUlogo.png new file mode 100644 index 00000000..fa05ebff Binary files /dev/null and b/Exercise 4/media/cropped-BUlogo.png differ diff --git a/Exercise 4/media/css3-logo-black-and-white.png b/Exercise 4/media/css3-logo-black-and-white.png new file mode 100644 index 00000000..8678f430 Binary files /dev/null and b/Exercise 4/media/css3-logo-black-and-white.png differ diff --git a/Exercise 4/media/css_ex2/style.css b/Exercise 4/media/css_ex2/style.css new file mode 100644 index 00000000..cf7bfd8e --- /dev/null +++ b/Exercise 4/media/css_ex2/style.css @@ -0,0 +1,127 @@ +/* palette variables (for later) */ +:root{ + --primary:#0077b6; /* deep blue */ + --accent:#00b4d8; /* cyan */ + --light:#f0f8ff; /* very light blue */ + --bg:#e8f4ff; /* page background */ + --border:#185b8c; /* darker blue */ + --table-head:#e63946;/* red */ + } + + /* base */ + body{ + margin:0; + font-family:Arial, Helvetica, sans-serif; /* Fonts I picked that resonate with me */ + background:var(--bg); + color:#222; + padding:0 12px 40px; + } + + /* reusable soft-card */ + .soft{ + background:#fff; + border:2px solid var(--border); + padding:20px; + margin:24px auto; + max-width:940px; + box-shadow:0 0 12px #0077b620; /* subtle bloom */ + } + + /* headings */ + h1{ + color:#fff; + background:var(--primary); + text-align:center; + margin:0 -12px 24px; + padding:28px 12px; + text-shadow:0 0 6px #ffffffaa; /* glowing effect */ + } + h2{ + color:var(--primary); + border-bottom:4px solid var(--accent); + padding-bottom:4px; + margin:28px auto 12px; + max-width:940px; + text-shadow:0 0 4px #00b4d880; + } + h3{ + margin:6px 0 24px; + color:var(--accent); + text-align:center; + text-shadow:0 0 4px #00b4d880; + } + + /* description paragraph */ + p:first-of-type{ + max-width:760px; + margin:0 auto 24px; + } + + /* table */ + table { + border-collapse: collapse; + width: 50%; + margin-left: auto; + margin-right: auto; + text-align: center; + } + + th,td{ + border:1px solid var(--accent); + padding:10px 6px; + text-align:center; + } + th{ + background:var(--table-head); + color:#fff; + } + + /* media thumbs */ + img{ + border:4px solid var(--accent); + padding:4px; + background:#fff; + box-shadow:0 0 8px #00b4d820; + } + + /* form */ + form{ + background:#fff; + border:2px solid var(--border); + padding:20px; + max-width:540px; + margin:24px auto; + box-shadow:0 0 10px #0077b620; + } + label{display:block;margin:10px 0;} + input,select,textarea{ + width:100%;max-width:100%; + padding:6px;border:1px solid #ccc;margin-top:4px; + } + fieldset{ + border:1px solid var(--accent); + padding:10px;margin:14px 0; + } + + /* Button CSS, background uses primary variable, no border, changes cursor to pointer */ + button{ + background:var(--primary); + color:#fff; + border:none; + padding:8px 18px; + margin-top:12px; + cursor:pointer; + } + /* This makes the reset button have a unique */ + button[type="reset"]{background:var(--accent);} + + /* This uses :hover to change colors */ + button:hover{ + background:var(--light); + color:#000000; + box-shadow:0 0 6px #00b4d8aa; + } + + /* figures */ + figure{margin:24px auto;max-width:320px;text-align:center;} + figcaption{margin-bottom:6px;font-weight:bold;} \ No newline at end of file diff --git a/Exercise 4/media/ex1.html b/Exercise 4/media/ex1.html new file mode 100644 index 00000000..a0f14dc2 --- /dev/null +++ b/Exercise 4/media/ex1.html @@ -0,0 +1,143 @@ + + + + + + Exercise 1 By Mohamad Dabbabo + + + + +

Exercise 1 by Mohamad Dabbabo

+

Date Submitted: 28/04/2025

+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
Some facts about me:
First NameLast NameFavorite MovieFavorite SongFavorite Video
MohamadDabbabo + +

Ghost in the Shell (1995)
Cyberpunk, AI, Philosophy, Identity

+ + Ghost in the Shell poster + +
+

Click “Play” to hear this concert performance
held in memory of the 2011 victims.

+ +
+ +
+ + +
+ + +

Enter your details below:

+ +
+ + +

+ + + +

+ + + +

+ + +
+ Available Days: +
+
+
+
+
+
+ +
+
+ + + +

+ + +
+ Working on a new website? +
+ +
+
+ + + +

+ + + + +
+ + + diff --git a/Exercise 4/media/ex2.html b/Exercise 4/media/ex2.html new file mode 100644 index 00000000..fff5b9c5 --- /dev/null +++ b/Exercise 4/media/ex2.html @@ -0,0 +1,114 @@ + + + + + + Robotics & AI Expo 2025 – Information Page + + + + + + + + +

Robotics & AI Expo 2025

+

Saturday · 24 May 2025 · Gesamtschule Konradsdorf · 10:00–17:00

+ + +

+ Ever wanted to unleash your AI skills? Join us for a day of hands-on workshops (practical), thrilling competitions (hackathons), and thought-provoking debates (About future AI)! + You will learn about the latest in robotics and AI, and have the chance to showcase your own projects. Even bring your arguments to the table! + So what are you waiting for? Come join the event! +

+ + +

Event Schedule

+ + + + + + + + + + + + + + + +
TimeActivityLocation
10 : 00Opening KeynoteAula
11 : 00Autonomous-Bot RaceSporthalle
13 : 00ML Workshop: "Teach a CNN to lower grade students"Lab 1B
15 : 30Debate – "Will AI surpass us?"Auditorium
16 : 45Awards & ClosingAula
+ + +

Sneak-peek

+

+ + Expo poster by GPT-4o image gen + +

+ + + + +
+
10-seconds highlight reel:
+ +
+ + + +

Register / Contact Us

+
+ + + + + + + + + +
+ Interested in: + + + +
+ + + + + +
+ + + + + diff --git a/Exercise 4/media/ex3.html b/Exercise 4/media/ex3.html new file mode 100644 index 00000000..525b324e --- /dev/null +++ b/Exercise 4/media/ex3.html @@ -0,0 +1,121 @@ + + + + + + + Exercise 3 – Advanced CSS Demo + + + + + + + +
+
First Name
+
Last Name
+
Favorite Movie
+
Favorite Song
+
Favorite Video
+ +
Mohamad
+
Dabbabo
+ +
+ +
+
+ +
+
+ + + + +
+
First Name
+
Mohamad
+ +
Last Name
+
Dabbabo
+ +
Favorite Movie
+ + +
Favorite Song
+
+ +
+ +
Favorite Video
+
+ +
+
+ + + +
+

Enter your details below:

+
+ + + + +
+ Available Meeting Days: +
+ + + + + + + +
+
+ + + +
+ Currently working on a new website? + + +
+ + + +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/Exercise 4/media/media_ex1/Kenji_Kawai_Making_of_a_Cyborg.mp3 b/Exercise 4/media/media_ex1/Kenji_Kawai_Making_of_a_Cyborg.mp3 new file mode 100644 index 00000000..f38739f7 Binary files /dev/null and b/Exercise 4/media/media_ex1/Kenji_Kawai_Making_of_a_Cyborg.mp3 differ diff --git a/Exercise 4/media/media_ex2/poster.png b/Exercise 4/media/media_ex2/poster.png new file mode 100644 index 00000000..d47417bd Binary files /dev/null and b/Exercise 4/media/media_ex2/poster.png differ diff --git a/Exercise 4/media/media_ex2/robotics_schedule.mp4 b/Exercise 4/media/media_ex2/robotics_schedule.mp4 new file mode 100644 index 00000000..57dd3454 Binary files /dev/null and b/Exercise 4/media/media_ex2/robotics_schedule.mp4 differ diff --git a/Exercise 4/media/media_ex3/background-desktop.jpg b/Exercise 4/media/media_ex3/background-desktop.jpg new file mode 100644 index 00000000..22ffcf85 Binary files /dev/null and b/Exercise 4/media/media_ex3/background-desktop.jpg differ diff --git a/Exercise 4/media/media_ex3/background-mobile.jpg b/Exercise 4/media/media_ex3/background-mobile.jpg new file mode 100644 index 00000000..e2c914f9 Binary files /dev/null and b/Exercise 4/media/media_ex3/background-mobile.jpg differ diff --git a/Exercise 4/media/media_ex3/marvel_studios.mp4 b/Exercise 4/media/media_ex3/marvel_studios.mp4 new file mode 100644 index 00000000..bcdbaa6b Binary files /dev/null and b/Exercise 4/media/media_ex3/marvel_studios.mp4 differ diff --git a/Exercise 4/media/media_ex3/rick_roll.mp3 b/Exercise 4/media/media_ex3/rick_roll.mp3 new file mode 100644 index 00000000..89f4bfd9 Binary files /dev/null and b/Exercise 4/media/media_ex3/rick_roll.mp3 differ diff --git a/Exercise 4/media/style_ex3.css b/Exercise 4/media/style_ex3.css new file mode 100644 index 00000000..b96c79da --- /dev/null +++ b/Exercise 4/media/style_ex3.css @@ -0,0 +1,178 @@ +/* ---------- Root + reset ---------- */ +*{box-sizing:border-box;margin:0;padding:0;} + +:root{ + --bg-dark:#0b1522; + --bg-accent:#111a2e; + --grid-border:#8a8fa3; + --panel-bg:#24323fbb; + + --btn-green:#42b067; + --btn-green-hi:#c3ffd7; + --btn-red:#c84848; + --btn-red-hi:#ff9e9e; + + --text-light:#e6e6e6; + --highlight:#ffdc64; +} + +body{ + font-family:system-ui,Segoe UI,Roboto,sans-serif; + color:var(--text-light); + background:url("media_ex3/background-desktop.jpg") center/cover fixed no-repeat; + padding:1rem; + line-height:1.5; +} + +/* ---------- Helpers ---------- */ +.visually-hidden{ + position:absolute!important; + clip:rect(1px,1px,1px,1px); + width:1px;height:1px;overflow:hidden; +} + +/* ---------- Layout sections ---------- */ +.page-header{ + text-align:center; + border-bottom:3px solid var(--text-light); + margin-bottom:1rem; +} +.page-header .highlight{color:var(--highlight);} +.page-header .sub{font-size:.8rem;opacity:.9;} + +.content-section{max-width:1280px;margin:2rem auto 0;} + +/* ---------- Fun-facts grids ---------- */ +.facts-grid{ + display:grid; + border:3px solid var(--grid-border); + background:var(--panel-bg); +} +.facts-grid .cell{ + padding:.75rem; + text-align:center; + border:2px solid var(--grid-border); +} +.facts-grid .head{background:var(--grid-border);font-weight:600;} +.facts-grid img{max-width:100%;height:auto;display:block;margin:0.5rem auto;} + +.cell.movie p{color:var(--highlight);font-style:italic;} +/* zebra accent */ +.facts-grid .cell:nth-child(10n+6), +.facts-grid .cell:nth-child(10n+7){background:#3a4857;} + +/* ---------- Form ---------- */ +.details-form{ + background:var(--panel-bg); + border:3px solid var(--grid-border); + padding:2rem 2.5rem; + margin:auto; + max-width:320px; + display:flex;flex-direction:column;gap:1rem; +} +.details-form label{display:flex;flex-direction:column;font-size:.9rem;gap:.25rem;} + +.details-form input:not([type=checkbox]):not([type=radio]), +.details-form select, +.details-form textarea{ + padding:.5rem; + border:1px solid #aaa; + border-radius:4px; +} +.details-form input:not([type=checkbox]):not([type=radio]):focus, +.details-form select:focus, +.details-form textarea:focus{outline:2px solid var(--highlight);} + + +/* remove default fieldset styles */ +.details-form fieldset{ + border:none; /* remove default ugly border */ + padding:0; + margin:0; + display:flex; + flex-direction:column; + gap:.5rem; +} + +/* radio button styles */ +.details-form fieldset label { + display: flex; /* for radio buttons */ + align-items: center; + gap: 0.5rem; /* space between radio and text */ + font-size: 0.9rem; + cursor: pointer; +} + +/* radio styles */ +.details-form input[type="radio"] { + accent-color: var(--highlight); + transform: scale(1.2); + cursor: pointer; +} + +.check-group{ + display:grid; + grid-template-columns:repeat(2,1fr); + gap:.5rem 1rem; + font-size:.9rem; + justify-items:center; + align-items:center; +} +.check-group label{display:flex;align-items:center;gap:.5rem;} + +/* buttons */ +.btn-row{display:flex;gap:.75rem;justify-content:center;} + +.btn{ + cursor:pointer;border:none; + padding:.5rem 1.5rem;border-radius:4px; + font-weight:600;color:#fff; + transition:transform .2s ease,background-color .2s ease; +} +.btn.submit{background:var(--btn-green);} +.btn.reset {background:var(--btn-red);} +.btn.submit:hover{ + transform:translateY(-2px); + background:var(--btn-green-hi); + color:var(--bg-dark); +} +.btn.reset:hover{ + transform:translateY(-2px); + background:var(--btn-red-hi); + color:var(--bg-dark); +} + +/* ---------- Footer ---------- */ +h2{text-align:center;} +.page-footer{margin-top:3rem;text-align:center;padding:.5rem;background:#800080aa;} +.page-footer .copy{font-size:.9rem;} + +/* ---------- Media queries ---------- */ + +/* Mobile <= 767 px */ +@media (max-width:767px){ + body{background:url("media_ex3/background-mobile.jpg") center/cover fixed no-repeat;} + + /* show mobile grid / hide desktop */ + .facts-grid-desktop{display:none;} + .facts-grid-mobile{ + grid-template-columns:1fr; + } + + .page-header h1{font-size:1.3rem;} + .details-form{max-width:100%;} +} + +/* Desktop >= 768 px */ +@media (min-width:768px){ + /* show desktop grid / hide mobile */ + .facts-grid-mobile{display:none;} + .facts-grid-desktop { + grid-template-columns: repeat(5, 1fr); + } + + .details-form{max-width:380px;} + + /* center poster a bit nicer on wide screens */ + .facts-grid-desktop .cell.movie img{width:240px;} +} diff --git a/Exercise 4/style.css b/Exercise 4/style.css new file mode 100644 index 00000000..4fea0e51 --- /dev/null +++ b/Exercise 4/style.css @@ -0,0 +1,134 @@ +/* === Global tweaks === */ +:root { + --bs-primary-rgb: 80, 34, 255; /* tweak bootstrap primary if desired */ + } + + body { + scroll-behavior: smooth; + } + + /* Section headings */ + .section-title { + font-weight: 700; + letter-spacing: 0.5px; + } + + /* ===== Hero ===== */ + .hero { + min-height: 100vh; + background: linear-gradient(135deg, rgba(100,0,255,0.8), rgba(0,0,0,0.8)), + url("media/ai_gradient_bg.jpg") center/cover no-repeat; + } + + /* ===== Contact section background ===== */ + .contact { + background: linear-gradient(135deg, rgba(0,0,0,0.8), rgba(102,0,255,0.8)), + url("media/ai_mesh_bg.jpg") center/cover fixed; + } + + /* ===== Custom button (black → charcoal) ===== */ + .btn-custom { + background: #000; + color: #fff; + border: none; + transition: background 0.3s; + } + + .btn-custom:hover, + .btn-custom:focus { + background: #333; + color: #fff; + } + + /* Navbar active link */ + .navbar-nav .nav-link.active { + font-weight: 600; + } + + /* Portfolio cards */ + .card-img-top { + height: 320px; + object-fit: contain; + } + + /* Make modals slightly translucent for style */ + .modal-content { + backdrop-filter: blur(4px); + } + + /* Custom Badge Colors */ + .badge-html { + background-color: #ff6600; /* vibrant HTML orange */ + color: white; + border-radius: 0.5rem; + transition: all 0.3s ease; + } + .badge-html:hover::after { + content: " • Started Apr 2025"; + font-size: 0.75rem; + color: #fff; + } + + .badge-css { + background-color: #2965f1; /* official CSS blue */ + color: white; + border-radius: 0.5rem; + transition: all 0.3s ease; + } + .badge-css:hover::after { + content: " • Started Apr 2025"; + font-size: 0.75rem; + color: #fff; + } + + .badge-python { + background-color: #306998; /* Python blue */ + color: #ffd343; /* Python yellow accent */ + border-radius: 0.5rem; + transition: all 0.3s ease; + } + .badge-python:hover::after { + content: " • Started 2023"; + font-size: 0.75rem; + color: #ffd343; + } + + .badge-csharp { + background-color: #68217a; /* deep purple C# */ + color: #ffffff; + border-radius: 0.5rem; + transition: all 0.3s ease; + } + .badge-csharp:hover::after { + content: " • Started 2023"; + font-size: 0.75rem; + color: #fff; + } + + .badge-latex { + background-color: #0f0f0f; /* dark, serious tone */ + color: #00ffee; /* glowing cyan for flair */ + border-radius: 0.5rem; + font-family: 'Courier New', monospace; + transition: all 0.3s ease; + } + .badge-latex:hover::after { + content: " • Started 2024"; + font-size: 0.75rem; + color: #00ffee; + } + + .badge-js { + background-color: #f7df1e; /* bright yellow JS */ + color: #000000; /* black text for contrast */ + border-radius: 0.5rem; + transition: all 0.3s ease; + } + .badge-js:hover::after { + content: " • Started May 2025"; + font-size: 0.75rem; + color: #000000; + } + + + \ No newline at end of file diff --git a/Exercise 5/index.html b/Exercise 5/index.html new file mode 100644 index 00000000..b697d2cf --- /dev/null +++ b/Exercise 5/index.html @@ -0,0 +1,40 @@ + + + + + + ADOS App | Cosmic AI Form Validator + + + + +
+ + +
+

ADOS APP VALIDATOR

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + + + diff --git a/Exercise 5/script.js b/Exercise 5/script.js new file mode 100644 index 00000000..c60ed0ce --- /dev/null +++ b/Exercise 5/script.js @@ -0,0 +1,80 @@ +/* ========== 1. Enhanced Particle Generator ========== */ +const starLayer = document.getElementById('starsLayer'); +const STAR_COUNT = 60; // double the stars + +for (let i = 0; i < STAR_COUNT; i++) { + const s = document.createElement('span'); + const size = Math.random() * 3 + 1; // 1–4 px + const dur = 15 + Math.random() * 15; // 15–30 s + const delay= Math.random() * -dur; // random start + const left = Math.random() * 100; // vw + const offset = Math.random() * -100 + 'vh'; // start above + const xShift = Math.random() * 40 - 20 + 'vw'; // drift + const opacity= Math.random() * .5 + .2; // .2–.7 + + s.style.width = s.style.height = size + 'px'; + s.style.left = left + 'vw'; + s.style.setProperty('--start', offset); + s.style.setProperty('--xShift', xShift); + s.style.animationDuration= dur + 's'; + s.style.animationDelay = delay + 's'; + s.style.setProperty('--o', opacity); + starLayer.appendChild(s); +} + +/* ========== 2. Validator Logic ========== */ +function formValidator(firstName, lastName, age, phoneNumber) { + const fields = [ + { val: firstName, label: 'First name', type: 'string' }, + { val: lastName, label: 'Last name', type: 'string' }, + { val: age, label: 'Age', type: 'number' }, + { val: phoneNumber, label: 'Phone number', type: 'string' } + ]; + for (const f of fields) { + if (f.val === undefined || f.val === null || f.val === '') { + return { ok: false, msg: `The ${f.label} input is missing.` }; + } + if (typeof f.val !== f.type) { + return { ok: false, msg: `The ${f.label} should be a ${f.type}.` }; + } + } + if (age < 18) { + return { ok: false, msg: 'Sorry, not old enough for our app.' }; + } + return { ok: true, msg: 'WELCOME TO THE ADOS APP.' }; +} + +/* ========== 3. UI Hooks ========== */ +const form = document.getElementById('validatorForm'); +const result = document.getElementById('resultMessage'); + +form.addEventListener('submit', e => { + e.preventDefault(); + + const firstName = document.getElementById('firstName').value.trim(); + const lastName = document.getElementById('lastName').value.trim(); + const ageInput = document.getElementById('age').value; + const age = ageInput === '' ? '' : Number(ageInput); + const phoneNumber = document.getElementById('phoneNumber').value.trim(); + + const { ok, msg } = formValidator(firstName, lastName, age, phoneNumber); + result.textContent = msg; + result.className = 'message ' + (ok ? 'success' : 'error') + ' show'; + + if (ok) { + const card = document.querySelector('.container'); + card.style.animation = 'pulse 1s'; + setTimeout(() => card.style.animation = '', 1000); + } +}); + +/* ========== 4. Inject Pulse Keyframes ========== */ +document.head.insertAdjacentHTML('beforeend', ` + +`); diff --git a/Exercise 5/styles.css b/Exercise 5/styles.css new file mode 100644 index 00000000..674eb237 --- /dev/null +++ b/Exercise 5/styles.css @@ -0,0 +1,203 @@ +/* ─────────── Global Reset & Tokens ─────────── */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + } + :root { + --violet-1: #0c0032; + --violet-2: #190061; + --violet-3: #240090; + --violet-4: #3500d3; + --neon-1: #4a00e0; + --neon-2: #8e2de2; + --cyan: #00e5ff; + } + + /* ─────────── Cosmic Background ─────────── */ + body { + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + color: #fff; + padding: 2rem; + background: linear-gradient( + 135deg, + var(--violet-1), + var(--violet-2), + var(--violet-3), + var(--violet-4) + ); + overflow: hidden; + position: relative; + } + body::before { + /* subtle nebula glow */ + content: ''; + position: fixed; + inset: 0; + background: radial-gradient( + circle at 60% 30%, + rgba(255, 0, 255, 0.15) 0%, + rgba(0, 0, 0, 0) 60% + ); + pointer-events: none; + z-index: -2; + } + + /* ─────────── Star Particles ─────────── */ + .particles { + position: fixed; + inset: 0; + z-index: -3; + overflow: hidden; + } + .particles span { + position: absolute; + display: block; + border-radius: 50%; + background: #fff; + animation: floating linear infinite; + opacity: var(--o); + } + @keyframes floating { + from { + transform: translateY(var(--start)) translateX(0); + } + to { + transform: translateY(calc(var(--start) + 100vh)) + translateX(var(--xShift)); + } + } + + /* ─────────── Glass Card ─────────── */ + .container { + width: 100%; + max-width: 600px; + backdrop-filter: blur(20px) saturate(160%); + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 16px; + padding: 2rem; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); + transition: transform 0.3s ease, box-shadow 0.3s ease; + } + .container:hover { + transform: translateY(-6px); + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5); + } + + /* ─────────── Typography ─────────── */ + h1 { + text-align: center; + margin-bottom: 1.5rem; + font-weight: 700; + letter-spacing: 1px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + } + + /* ─────────── Form Layout ─────────── */ + form { + display: grid; + gap: 1.5rem; + } + .form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + label { + font-weight: 600; + font-size: 1rem; + color: rgba(255, 255, 255, 0.9); + } + + /* ─────────── Inputs ─────────── */ + input { + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.25); + color: #fff; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + font-size: 1rem; + transition: background 0.3s, border-color 0.3s, box-shadow 0.3s; + } + input::placeholder { + color: #cfcfff; + } + input:focus { + outline: none; + background: rgba(255, 255, 255, 0.18); + border-color: var(--cyan); + box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.25); + } + + /* ─────────── CTA Button ─────────── */ + button { + background: linear-gradient( + to right, + var(--neon-1), + var(--neon-2) + ); + border: none; + border-radius: 0.5rem; + color: #fff; + padding: 0.75rem; + font-size: 1rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + cursor: pointer; + transition: transform 0.3s, box-shadow 0.3s; + margin-top: 0.5rem; + } + button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); + } + + /* ─────────── Validation Message ─────────── */ + .message { + margin-top: 1.5rem; + padding: 1rem; + border-radius: 0.5rem; + text-align: center; + font-weight: 600; + height: 3.5rem; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s; + } + .message.show { + opacity: 1; + } + .error { + background: rgba(255, 87, 87, 0.2); + border: 1px solid rgba(255, 87, 87, 0.5); + color: #ff5757; + } + .success { + background: rgba(87, 255, 154, 0.2); + border: 1px solid rgba(87, 255, 154, 0.5); + color: #57ff9a; + } + + /* ─────────── Responsiveness ─────────── */ + @media (max-width: 768px) { + .container { + padding: 1.5rem; + } + } + @media (max-width: 480px) { + body { + padding: 1rem; + } + h1 { + font-size: 1.5rem; + } + } + \ No newline at end of file diff --git a/Exercise 6/App 1/index.html b/Exercise 6/App 1/index.html new file mode 100644 index 00000000..61ad14a3 --- /dev/null +++ b/Exercise 6/App 1/index.html @@ -0,0 +1,84 @@ + + + + + + + + Cosmic AI • Form Validator + + + + + + + + + + + + + + + +
+
+

COSMIC Validator

+

Powered by the Sentineurons  •  Lesson 6

+
+ + +
+
+ + + + + + + + + + +
+ + +
+ + + +
+ + + + diff --git a/Exercise 6/App 1/script.js b/Exercise 6/App 1/script.js new file mode 100644 index 00000000..5579f32c --- /dev/null +++ b/Exercise 6/App 1/script.js @@ -0,0 +1,153 @@ +/* ───────────────────────────────────────────────────────── + 1. Starfield Animation + ───────────────────────────────────────────────────────── */ + class Starfield { + constructor(canvasId, speed, density) { + this.canvas = document.getElementById(canvasId); + this.ctx = this.canvas.getContext("2d"); + this.speed = speed; + this.density = density; + this.stars = []; + this.resize(); + addEventListener("resize", () => this.resize()); + this.populate(); + requestAnimationFrame(() => this.animate()); + } + resize() { + this.canvas.width = innerWidth; + this.canvas.height = innerHeight; + } + populate() { + this.stars.length = 0; + const count = (innerWidth + innerHeight) / this.density; + for (let i = 0; i < count; i++) this.stars.push(this.newStar()); + } + newStar() { + return { + x: Math.random() * this.canvas.width, + y: Math.random() * this.canvas.height, + r: Math.random() * 1.2 + 0.2, + o: Math.random() * 0.7 + 0.2, + }; + } + animate() { + const { ctx, canvas, stars, speed } = this; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#fff"; + stars.forEach((s) => { + ctx.globalAlpha = s.o; + ctx.beginPath(); + ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); + ctx.fill(); + s.y += speed; + if (s.y > canvas.height) { + s.y = -s.r; + s.x = Math.random() * canvas.width; + } + }); + requestAnimationFrame(() => this.animate()); + } + } + + // Create starfields + new Starfield("farStars", 0.3, 8); + new Starfield("nearStars", 0.7, 4); + + /* ───────────────────────────────────────────────────────── + 2. Validator (Lesson 5 Logic) + ───────────────────────────────────────────────────────── */ + function formValidator(firstName, lastName, age, phoneNumber) { + const fields = [ + { val: firstName, label: "First name", type: "string" }, + { val: lastName, label: "Last name", type: "string" }, + { val: age, label: "Age", type: "number" }, + { val: phoneNumber, label: "Phone number", type: "string" }, + ]; + + // Check for missing / type mismatch + for (const f of fields) { + if (f.val === undefined || f.val === null || f.val === "") { + return { ok: false, msg: `The ${f.label} input is missing.` }; + } + if (typeof f.val !== f.type) { + return { ok: false, msg: `The ${f.label} should be a ${f.type}.` }; + } + } + if (age < 18) { + return { ok: false, msg: "Sorry, not old enough for our app." }; + } + return { ok: true, msg: "WELCOME TO THE ADOS APP." }; + } + + /* ───────────────────────────────────────────────────────── + 3. UI Interaction + ───────────────────────────────────────────────────────── */ + const form = document.getElementById("cosmicForm"); + const banner = document.getElementById("resultBanner"); + const bannerText = document.getElementById("resultText"); + + form.addEventListener("submit", (e) => { + e.preventDefault(); + + // Grab + trim values + const firstName = document.getElementById("firstName").value.trim(); + const lastName = document.getElementById("lastName").value.trim(); + const ageInput = document.getElementById("age").value; + const age = ageInput === "" ? "" : Number(ageInput); + const phoneNumber = document.getElementById("phoneNumber").value.trim(); + + // Core validation + const { ok, msg } = formValidator(firstName, lastName, age, phoneNumber); + + // Reset field states + [...form.querySelectorAll(".field")].forEach((f) => + f.classList.remove("error", "valid", "show") + ); + + // Simple per‑field error visual (missing / type mismatch) + if (!ok) { + if (msg.includes("First name")) showError("firstName", msg); + else if (msg.includes("Last name")) showError("lastName", msg); + else if (msg.includes("Age")) showError("age", msg); + else if (msg.includes("Phone number")) showError("phoneNumber", msg); + } else { + // Mark all good + [...form.querySelectorAll(".field")].forEach((f) => { + f.classList.add("valid", "show"); + f.querySelector("small").textContent = "Looks good!"; + }); + } + + // Banner feedback + bannerText.textContent = msg; + banner.className = "banner " + (ok ? "success" : "error") + " show"; + banner.hidden = false; + + // Subtle pulse on success + if (ok) { + form.parentElement.style.animation = "pulse 0.9s"; + setTimeout(() => (form.parentElement.style.animation = ""), 900); + } + }); + + /* Helper */ + function showError(id, msg) { + const field = document.getElementById(id).closest(".field"); + field.classList.add("error", "show"); + field.querySelector("small").textContent = msg; + } + + /* ───────────────────────────────────────────────────────── + 4. Inject Pulse Animation + ───────────────────────────────────────────────────────── */ + document.head.insertAdjacentHTML( + "beforeend", + `` + ); + \ No newline at end of file diff --git a/Exercise 6/App 1/styles.css b/Exercise 6/App 1/styles.css new file mode 100644 index 00000000..4bead9f7 --- /dev/null +++ b/Exercise 6/App 1/styles.css @@ -0,0 +1,293 @@ +/* ───────────────────────────────────────────────────────── + 0. Theme Tokens + ───────────────────────────────────────────────────────── */ + :root { + /* Cosmic gradient hues */ + --h1: #190061; + --h2: #240090; + --h3: #3500d3; + --n1: #4a00e0; + --n2: #8e2de2; + --cy: #00e5ff; + --cosmic: linear-gradient(135deg, var(--h1), var(--h2), var(--h3)); + + /* Glass morphism */ + --glass: rgba(255, 255, 255, 0.08); + --glass-border: rgba(255, 255, 255, 0.18); + + /* Typography */ + --font: "Poppins", system-ui, sans-serif; + } + + /* ───────────────────────────────────────────────────────── + 1. Reset & Base + ───────────────────────────────────────────────────────── */ + *, + *::before, + *::after { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + font-family: var(--font); + color: #fff; + background: linear-gradient(135deg, var(--h1), var(--h2), var(--h3)); + overflow: hidden; + } + + /* Scrollbar hide (desktop) */ + body::-webkit-scrollbar { + width: 0 !important; + } + + /* ───────────────────────────────────────────────────────── + 2. Starfield Background + ───────────────────────────────────────────────────────── */ + canvas { + position: fixed; + inset: 0; + z-index: -3; + } + + /* ───────────────────────────────────────────────────────── + 3. Glass Card + ───────────────────────────────────────────────────────── */ + .card { + width: min(90vw, 640px); + padding: 2.5rem 2rem 3rem; + border-radius: 1.5rem; + backdrop-filter: blur(25px) saturate(165%); + background: var(--glass); + border: 1px solid var(--glass-border); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); + animation: popIn 0.9s cubic-bezier(0.16, 1, 0.3, 1); + } + + @keyframes popIn { + from { + opacity: 0; + transform: translateY(40px) scale(0.97); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + + /* Header */ + .card__header { + text-align: center; + margin-bottom: 2rem; + } + + /* Title */ + .card__title { + display: inline-block; /* so background is one chunk */ + background: linear-gradient( + 90deg, + yellow, + var(--n2), + var(--cy), + var(--n1) + ); + background-size: 200% 200%; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + animation: gradientShift 4s ease infinite; + } + + /* 2) Have the span just inherit that same background */ + .card__title span { + background: inherit; /* grab the parent’s gradient */ + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + /* 3) Your existing keyframes (no change) */ + @keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } + } + + .card__title span { + opacity: 0.85; + } + + .card__subtitle { + font-size: 0.85rem; + letter-spacing: 0.05em; + opacity: 0.75; + margin-top: 0.25rem; + } + + /* ───────────────────────────────────────────────────────── + 4. Form Grid + ───────────────────────────────────────────────────────── */ + form .grid { + display: grid; + gap: 1.5rem 1.25rem; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + } + + /* Field wrapper */ + .field { + display: flex; + flex-direction: column; + gap: 0.5rem; + position: relative; + } + + .field span { + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 0.03em; + } + + .field input { + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 0.65rem; + padding: 0.9rem 1rem; + font-size: 1rem; + color: #fff; + transition: border-color 0.3s, box-shadow 0.3s, background 0.25s; + } + + .field input::placeholder { + color: #ccd; + opacity: 0.75; + } + + .field input:focus { + outline: none; + border-color: var(--cy); + box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.25); + background: rgba(255, 255, 255, 0.18); + } + + /* Error / success hints */ + .field small { + position: absolute; + bottom: -1.25rem; + left: 0; + font-size: 0.75rem; + font-weight: 600; + opacity: 0; + transform: translateY(4px); + transition: opacity 0.3s, transform 0.3s; + } + + .field.error small { + color: #ff6464; + } + + .field.valid small { + color: #57ff9a; + } + + .field.show small { + opacity: 1; + transform: translateY(0); + } + + /* ───────────────────────────────────────────────────────── + 5. CTA Button + ───────────────────────────────────────────────────────── */ + .cta { + margin-inline: auto; + margin-top: 2.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.9rem 3.5rem; + font-size: 1rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #fff; + background: linear-gradient(120deg, var(--n1), var(--n2), var(--cy)); + border: none; + border-radius: 0.75rem; + cursor: pointer; + overflow: hidden; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + .cta .glow { + pointer-events: none; + position: absolute; + inset: 0; + background: radial-gradient(circle at center, rgba(255, 255, 255, 0.25) 0%, transparent 70%); + opacity: 0; + transform: scale(0.2); + transition: opacity 0.4s, transform 0.4s; + } + + .cta:hover { + transform: translateY(-4px); + } + + .cta:hover .glow { + opacity: 1; + transform: scale(1.6); + } + + .cta:active { + transform: translateY(-1px); + } + + /* ───────────────────────────────────────────────────────── + 6. Result Banner + ───────────────────────────────────────────────────────── */ + .banner { + margin-top: 2rem; + padding: 1.05rem 1.25rem; + border-radius: 0.75rem; + font-size: 1rem; + font-weight: 700; + text-align: center; + letter-spacing: 0.03em; + opacity: 0; + transform: translateY(10px); + transition: opacity 0.45s, transform 0.45s; + } + + .banner.show { + opacity: 1; + transform: translateY(0); + } + + .banner.error { + background: rgba(255, 87, 87, 0.15); + border: 1px solid #ff5757; + color: #ff8686; + } + + .banner.success { + background: rgba(87, 255, 154, 0.15); + border: 1px solid #57ff9a; + color: #a6ffce; + } + + /* ───────────────────────────────────────────────────────── + 7. Media Queries + ───────────────────────────────────────────────────────── */ + @media (max-width: 480px) { + .card { + padding: 2rem 1.5rem 2.5rem; + } + .card__title { + font-size: 1.65rem; + } + } + \ No newline at end of file diff --git a/Exercise 6/App 2/index.html b/Exercise 6/App 2/index.html new file mode 100644 index 00000000..bda7ee0b --- /dev/null +++ b/Exercise 6/App 2/index.html @@ -0,0 +1,67 @@ + + + + + + Cosmic HR • Employee Entry + + + + + + + + + + + + + +
+
+

Employee Entry App

+

Add • Edit • Delete in light‑speed

+
+ + +
+
+ + + + + + + +
+ + +
+ + +
+
+ + + + diff --git a/Exercise 6/App 2/script.js b/Exercise 6/App 2/script.js new file mode 100644 index 00000000..83cc70c3 --- /dev/null +++ b/Exercise 6/App 2/script.js @@ -0,0 +1,101 @@ +/************************************************************************** + * 1. Starfield (re‑use tiny parallax engine) * + **************************************************************************/ +class Stars{constructor(id,speed,density){ + this.c=document.getElementById(id);this.x=this.c.getContext("2d"); + this.speed=speed;this.density=density;this.stars=[];this.resize(); + addEventListener("resize",()=>this.resize());this.populate();this.loop()} + resize(){this.c.width=innerWidth;this.c.height=innerHeight} + populate(){this.stars.length=0;const n=(innerWidth+innerHeight)/this.density; + for(let i=0;i{this.x.globalAlpha=s.o;this.x.beginPath();this.x.arc(s.x,s.y,s.r,0,Math.PI*2);this.x.fill();s.y+=this.speed;if(s.y>this.c.height){s.y=-s.r;s.x=Math.random()*this.c.width}});requestAnimationFrame(()=>this.loop())}} + new Stars("bgFar",.3,8);new Stars("bgNear",.8,4); + + /************************************************************************** + * 2. Employee CRUD * + **************************************************************************/ + const form=document.getElementById("employeeForm"), + entriesEl=document.getElementById("entries"), + submitBtn=document.getElementById("submitBtn"); + + let editingId=null; // track current edit + + // Helpers + const randId=()=>crypto.randomUUID(); + const createTag=(tag,cls,txt)=>{const el=document.createElement(tag);if(cls)el.className=cls;if(txt)el.textContent=txt;return el} + + // Render one entry card + function renderEntry({id,name,age,sex,position}){ + const card=createTag("div","entry");card.dataset.id=id; + + const info=createTag("div","entry__info"); + info.append(createTag("strong","",`${name} • ${position}`)); + info.append(createTag("span","",`Age ${age} | ${sex}`)); + card.append(info); + + const btns=createTag("div","entry__btns"); + const edit=createTag("button","btn btn--edit","Edit"); + const del =createTag("button","btn btn--delete","Delete"); + btns.append(edit,del);card.append(btns); + + // Edit handler + edit.onclick=()=>{ + const {name,age,sex,position}=db.find(e=>e.id===id); + form.name.value=name;form.age.value=age;form.sex.value=sex;form.position.value=position; + submitBtn.querySelector(".txt").textContent="Update"; + editingId=id; + }; + + // Delete handler + del.onclick=()=>{ + db=db.filter(e=>e.id!==id); + card.style.animation="fadeOut .4s forwards"; + setTimeout(()=>card.remove(),380); + }; + + entriesEl.prepend(card); + } + + /* Fade‑out keyframes injected once */ + document.head.insertAdjacentHTML("beforeend",``); + + /************************************************************************** + * 3. DB + Form Handling * + **************************************************************************/ + let db=[]; + + form.addEventListener("submit",e=>{ + e.preventDefault(); + const name=form.name.value.trim(), + age =Number(form.age.value), + sex =form.sex.value, + position=form.position.value.trim(); + if(!name||!age||!sex||!position||age<18||age>100)return shakeForm(); + + if(editingId){ + // update + const rec=db.find(e=>e.id===editingId); + Object.assign(rec,{name,age,sex,position}); + refreshUI(); + editingId=null; + submitBtn.querySelector(".txt").textContent="Add Entry"; + }else{ + // create + const entry={id:randId(),name,age,sex,position}; + db.push(entry); + renderEntry(entry); + } + form.reset();form.name.focus(); + }); + + // UI Helpers + function refreshUI(){ + entriesEl.innerHTML="";db.forEach(renderEntry); + } + function shakeForm(){ + form.classList.add("shake");setTimeout(()=>form.classList.remove("shake"),500); + } + \ No newline at end of file diff --git a/Exercise 6/App 2/styles.css b/Exercise 6/App 2/styles.css new file mode 100644 index 00000000..737ccd6b --- /dev/null +++ b/Exercise 6/App 2/styles.css @@ -0,0 +1,296 @@ +/* ================================================ + COSMIC DESIGN SYSTEM + A modern, glassmorphic UI with cosmic gradient theme + ================================================ */ + + :root { + /* Color palette */ + --v1: #190061; /* Deep violet */ + --v2: #240090; /* Medium violet */ + --v3: #3500d3; /* Bright violet */ + --n1: #4a00e0; /* Neon purple */ + --n2: #8e2de2; /* Vibrant purple */ + --cy: #00e5ff; /* Cyan */ + + /* UI Elements */ + --glass: rgba(255, 255, 255, 0.07); /* Glass effect background */ + --border: rgba(255, 255, 255, 0.18); /* Subtle borders */ + --font: "Poppins", system-ui, sans-serif; + } + + /* ================================================ + BASE STYLES & RESET + ================================================ */ + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background: linear-gradient(135deg, var(--v1), var(--v2), var(--v3)); + color: #fff; + font-family: var(--font); + overflow: hidden; + } + + canvas { + position: fixed; + inset: 0; + z-index: -3; + } + + /* ================================================ + CARD COMPONENT + ================================================ */ + .card { + width: min(92vw, 720px); + padding: 2.5rem 2rem 3rem; + border-radius: 1.5rem; + backdrop-filter: blur(28px) saturate(170%); + background: var(--glass); + border: 1px solid var(--border); + box-shadow: 0 22px 65px rgba(0, 0, 0, 0.45); + animation: fadeIn 0.9s cubic-bezier(0.16, 1, 0.3, 1); + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(40px); + } + to { + opacity: 1; + transform: none; + } + } + + /* ================================================ + CARD HEADER + ================================================ */ + .card__header { + text-align: center; + margin-bottom: 2rem; + } + + .card__title { + font-size: 2rem; + font-weight: 700; + line-height: 1.1; + display: inline-block; + background: linear-gradient(90deg, var(--n1), var(--n2), var(--cy), var(--n1)); + background-size: 200% 200%; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + animation: gradShift 5s ease infinite; + } + + .card__title span { + background: inherit; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + } + + @keyframes gradShift { + 0% { + background-position: 0 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0 50%; + } + } + + .card__subtitle { + font-size: 0.85rem; + opacity: 0.75; + margin-top: 0.4rem; + } + + /* ================================================ + FORM ELEMENTS + ================================================ */ + form .grid { + display: grid; + gap: 1.5rem 1.25rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + } + + .field { + display: flex; + flex-direction: column; + gap: 0.55rem; + } + + .field span { + font-weight: 600; + font-size: 0.9rem; + letter-spacing: 0.03em; + } + + input, + select { + padding: 0.9rem 1rem; + border-radius: 0.65rem; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.25); + color: #fff; + font-size: 1rem; + transition: border-color 0.3s, box-shadow 0.3s, background 0.25s; + } + + input::placeholder { + color: #ccd; + opacity: 0.75; + } + + input:focus, + select:focus { + outline: none; + border-color: var(--cy); + box-shadow: 0 0 0 3px rgba(0, 229, 255, 0.25); + background: rgba(255, 255, 255, 0.18); + } + + /* ================================================ + CALL TO ACTION BUTTON + ================================================ */ + .cta { + margin: 2.5rem auto 0; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.9rem 3.5rem; + font-size: 1rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: #fff; + background: linear-gradient(120deg, var(--n1), var(--n2), var(--cy)); + border: none; + border-radius: 0.75rem; + cursor: pointer; + overflow: hidden; + position: relative; /* Added missing position property */ + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + .cta .glow { + position: absolute; + inset: 0; + background: radial-gradient(circle, rgba(255, 255, 255, 0.25) 0%, transparent 70%); + opacity: 0; + transform: scale(0.2); + transition: opacity 0.4s, transform 0.4s; + } + + .cta:hover { + transform: translateY(-4px); + } + + .cta:hover .glow { + opacity: 1; + transform: scale(1.6); + } + + .cta:active { + transform: translateY(-1px); + } + + /* ================================================ + ENTRIES LIST + ================================================ */ + .entries { + margin-top: 3rem; + display: flex; + flex-direction: column; + gap: 1.25rem; + } + + .entry { + position: relative; + padding: 1.25rem 1rem 1.25rem 1.25rem; + border: 1px solid var(--border); + border-radius: 1rem; + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(12px); + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 0.5rem; + animation: slideIn 0.5s ease; + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: none; + } + } + + .entry__info { + display: flex; + flex-direction: column; + gap: 0.2rem; + font-size: 0.95rem; + } + + .entry__info span { + opacity: 0.85; + } + + .entry__btns { + display: flex; + gap: 0.6rem; + } + + .btn { + padding: 0.45rem 0.7rem; + font-size: 0.85rem; + font-weight: 600; + border: none; + border-radius: 0.55rem; + cursor: pointer; + transition: transform 0.25s, box-shadow 0.25s; + background: rgba(255, 255, 255, 0.1); + color: #fff; + } + + .btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); + } + + .btn--edit { + background: linear-gradient(135deg, #ffd166, #ff8a3c); + } + + .btn--delete { + background: linear-gradient(135deg, #ef476f, #d7263d); + } + + /* ================================================ + RESPONSIVE STYLES + ================================================ */ + @media (max-width: 480px) { + .card { + padding: 2rem 1.5rem 2.5rem; + } + + .card__title { + font-size: 1.65rem; + } + } \ No newline at end of file diff --git a/Exercise 6/App 3/index.html b/Exercise 6/App 3/index.html new file mode 100644 index 00000000..b86ba852 --- /dev/null +++ b/Exercise 6/App 3/index.html @@ -0,0 +1,22 @@ + + + + + + + + Retro-Futuristic AI Chat + + + + + + + + + +
+ + + + diff --git a/Exercise 6/App 3/script.js b/Exercise 6/App 3/script.js new file mode 100644 index 00000000..9aa359c3 --- /dev/null +++ b/Exercise 6/App 3/script.js @@ -0,0 +1,191 @@ +/* ======================================================== + Retro-Futuristic AI Chat Application - script.js + Dynamically builds the UI, handles chat API, and animates starfield background. + -------------------------------------------------------- + Author: Your Name + License: Open (no API key needed for ChatGPT proxy) +======================================================== */ + +const API_URL = 'https://chatgpt-api.shn.hk/v1/'; + +// Configuration for the chat and background +const MODEL = 'gpt-3.5-turbo'; +let messageHistory = []; // Array to hold the conversation (chat history) + +// Starfield animation parameters +const STAR_COUNT = 100; +const STAR_SPEED = 0.5; +const STAR_COLOR_PALETTE = ['#ffffff', '#a0e0ff', '#80c0ff', '#c0c0ff']; // Neon-ish whites/blues +let stars = []; + +// Get references to initial HTML elements (the only static ones) +const startBtn = document.getElementById('startBtn'); +const chatContainer = document.getElementById('chatContainer'); + +/* Create and configure the canvas for the starfield background */ +let canvas, ctx; +function createStarfield() { + // Create canvas element and add to body + canvas = document.createElement('canvas'); + canvas.id = 'starfield'; + document.body.appendChild(canvas); + ctx = canvas.getContext('2d'); + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + // Initialize star objects at random positions and velocities + for (let i = 0; i < STAR_COUNT; i++) { + stars.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + radius: Math.random() * 1.2 + 0.5, + speed: STAR_SPEED + Math.random() * 0.5, + color: STAR_COLOR_PALETTE[Math.floor(Math.random() * STAR_COLOR_PALETTE.length)] + }); + } + + // Start the animation loop + animateStarfield(); +} + +/* Resize the canvas to fill the browser window */ +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} + +/* The main animation loop for the starfield background */ +function animateStarfield() { + // Clear canvas with a translucent fill for motion-trail effect + ctx.fillStyle = 'rgba(10, 10, 20, 0.3)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw and update each star + for (let star of stars) { + // Move star downward + star.y += star.speed; + // Reset star to top if it moves off bottom + if (star.y > canvas.height) { + star.y = 0; + star.x = Math.random() * canvas.width; + } + // Draw the star (as a circle) + ctx.beginPath(); + ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2); + ctx.fillStyle = star.color; + ctx.fill(); + } + + // Continue the animation + requestAnimationFrame(animateStarfield); +} + +// Initialize starfield on load +createStarfield(); + +/* ========== Chat UI Creation ========== */ +// On clicking the start button, build the chat interface dynamically +startBtn.addEventListener('click', () => { + // Remove or hide the start button + startBtn.style.display = 'none'; + + // Build the chat window and input field + const messagesDiv = document.createElement('div'); + messagesDiv.classList.add('messages'); + chatContainer.appendChild(messagesDiv); + + const inputArea = document.createElement('div'); + inputArea.id = 'inputArea'; + const chatInput = document.createElement('input'); + chatInput.id = 'chatInput'; + chatInput.placeholder = 'Type your message...'; + chatInput.autofocus = true; + const sendBtn = document.createElement('button'); + sendBtn.id = 'sendBtn'; + sendBtn.textContent = 'Send'; + inputArea.appendChild(chatInput); + inputArea.appendChild(sendBtn); + chatContainer.appendChild(inputArea); + + // Reference for adding new messages + window.chatWindow = messagesDiv; + + // Handler to send a message to the chat API + function sendChat() { + const userText = chatInput.value.trim(); + if (userText === '') return; + // Show user's message bubble + addMessage(userText, 'user'); + // Append user message to history for context + messageHistory.push({ role: 'user', content: userText }); + chatInput.value = ''; + + // Prepare the payload for the API (includes conversation history) + const payload = { + model: MODEL, + messages: [...messageHistory] + }; + + // Send the request to the ChatGPT API proxy + fetch(API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }) + .then(response => { + if (!response.ok) throw new Error('Network response was not OK'); + return response.json(); + }) + .then(data => { + // Extract AI assistant's reply + const aiReply = data.choices && data.choices[0]?.message?.content; + if (aiReply) { + messageHistory.push({ role: 'assistant', content: aiReply }); + addMessage(aiReply, 'bot'); + } else { + throw new Error('No reply from AI'); + } + }) + .catch(err => { + console.error('Chat API error:', err); + // Show a fallback error message to the user + const errorMsg = 'Sorry, the AI service is unavailable.'; + addMessage(errorMsg, 'bot'); + }); + } + + // Send message on button click or Enter key press + sendBtn.addEventListener('click', sendChat); + chatInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') sendChat(); + }); +}); + +/* ========================================================= + Utility: Add message to chat window with typewriter effect +========================================================= */ +function addMessage(text, sender) { + const messageElem = document.createElement('div'); + messageElem.classList.add('message', sender); + chatWindow.appendChild(messageElem); + // Type out the message text character-by-character + typeWriter(messageElem, text, 0); + // Scroll chat to bottom after adding + chatWindow.scrollTop = chatWindow.scrollHeight; +} + +/* Typewriter animation for messages */ +function typeWriter(element, text, index) { + if (index < text.length) { + element.textContent += text.charAt(index); + setTimeout(() => typeWriter(element, text, index + 1), 20); + } +} + +/* ======================================================================================== + End of script.js - all functions defined above + Thank you for reviewing the code! +======================================================================================== */ +/* +... Additional filler comments (omitted for brevity) ... +*/ diff --git a/Exercise 6/App 3/style.css b/Exercise 6/App 3/style.css new file mode 100644 index 00000000..524b72e3 --- /dev/null +++ b/Exercise 6/App 3/style.css @@ -0,0 +1,192 @@ +/* ======================================================= + Retro-Futuristic Chat - styles.css + Dark theme, neon glow effects, responsive design +======================================================= */ + +/* Neon Palette Colors: + #0ff (cyan) for user bubbles, + #f0f (magenta) for bot bubbles, + #08f (blue) for UI highlights. */ + +/* Base resets and body background */ +body { + margin: 0; + padding: 0; + background: #111; /* Dark gray background for dark theme */ + color: #eee; /* Light gray text color */ + font-family: 'Arial', sans-serif; + overflow: hidden; /* Hide scrollbars for full-screen canvas */ + } + + /* Chat container panel */ + #chatContainer { + position: relative; + z-index: 2; /* Above the canvas background */ + max-width: 600px; + margin: 50px auto; + padding: 20px; + background: rgba(20, 20, 30, 0.9); /* Semi-transparent dark panel */ + border-radius: 10px; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); + display: flex; + flex-direction: column; + } + + /* ======================================================= + Button Styles (Start and Send) + ======================================================= */ + + #startBtn { + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: #0ff; /* Neon cyan background */ + color: #000; + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 1.2rem; + cursor: pointer; + box-shadow: 0 0 10px #0ff; + transition: transform 0.2s ease, box-shadow 0.2s ease; + } + /* Hover and focus for start button */ + #startBtn:hover, #startBtn:focus { + box-shadow: 0 0 20px #0ff; + transform: translateX(-50%) scale(1.05); + } + + /* ======================================================= + Chat Messages (Bubbles) + ======================================================= */ + + .messages { + flex: 1; + overflow-y: auto; + margin-bottom: 10px; + padding-right: 10px; /* Padding for scrollbar */ + } + + .message { + margin: 8px 0; + padding: 10px 15px; + max-width: 80%; + border-radius: 15px; + position: relative; + font-size: 1rem; + line-height: 1.4; + word-wrap: break-word; + /* Neon text glow using multiple text-shadows */ + color: #fff; + text-shadow: + 0 0 5px rgba(255,255,255,0.8), + 0 0 10px rgba(0,255,255,0.6), + 0 0 15px rgba(0,255,255,0.4); + background: rgba(0, 50, 50, 0.8); + box-shadow: 0 0 10px rgba(0, 200, 255, 0.3); + } + + .message.user { + align-self: flex-end; + background: rgba(0, 60, 80, 0.9); + color: #0ff; + text-shadow: + 0 0 5px #0ff, + 0 0 10px #0ff, + 0 0 20px #0ff; + box-shadow: 0 0 10px rgba(0, 255, 255, 0.5); + } + + .message.bot { + align-self: flex-start; + background: rgba(80, 0, 60, 0.9); + color: #f0f; + text-shadow: + 0 0 5px #f0f, + 0 0 10px #f0f, + 0 0 20px #f0f; + box-shadow: 0 0 10px rgba(255, 0, 255, 0.5); + } + + /* ======================================================= + Input Field and Send Button + ======================================================= */ + + #inputArea { + display: flex; + margin-top: 10px; + } + #chatInput { + flex: 1; + padding: 10px; + border: 2px solid #0ff; + border-radius: 5px; + outline: none; + background: rgba(30, 30, 40, 0.8); + color: #fff; + font-size: 1rem; + box-shadow: inset 0 0 10px rgba(0, 255, 255, 0.2); + transition: box-shadow 0.2s ease, border-color 0.2s ease; + } + /* Glow on focus for the text input */ + #chatInput:focus { + border-color: #0ff; + box-shadow: 0 0 10px #0ff; + } + #sendBtn { + margin-left: 5px; + padding: 10px 15px; + border: none; + border-radius: 5px; + background: #f0f; + color: #000; + font-size: 1rem; + cursor: pointer; + box-shadow: 0 0 10px #f0f; + transition: box-shadow 0.2s ease, transform 0.2s ease; + } + #sendBtn:hover, #sendBtn:focus { + box-shadow: 0 0 20px #f0f; + transform: scale(1.05); + } + + /* ======================================================= + Responsive Layout Adjustments + ======================================================= */ + + @media (max-width: 600px) { + #chatContainer { + width: 100%; + margin: 0; + border-radius: 0; + max-width: none; + box-shadow: none; + } + #startBtn { + top: 10px; + } + } + + /* ======================================================= + Example flicker keyframes for neon effect (commented out) + ======================================================= */ + /* @keyframes flicker { */ + /* 0% { text-shadow: 0 0 10px #0ff; } */ + /* 50% { text-shadow: 0 0 20px #0ff; } */ + /* 100% { text-shadow: 0 0 10px #0ff; } */ + /* } */ + + /* ======================================================= + Canvas Starfield Background + ======================================================= */ + + #starfield { + position: fixed; + top: 0; + left: 0; + z-index: 0; + } + + /* End of styles.css */ + \ No newline at end of file diff --git a/Exercise 7/.gitignore b/Exercise 7/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/Exercise 7/.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/Exercise 7/README.md b/Exercise 7/README.md new file mode 100644 index 00000000..2dea69ea --- /dev/null +++ b/Exercise 7/README.md @@ -0,0 +1,71 @@ +# React_Lesson_Exercise +Welcome to the React 18 exercise. Upon completion of this exercise, you'll have permission to join the 'New Website' team, where you can contribute to a [real-world React application](https://github.com/buildingu/bu-front) for building-U. + +## Prerequisites +- Completed both JavaScript lessons. +- Node.js and NPM installed. +- Git installed + +## Objective +Demonstrate your understanding of various React hooks through a series of challenges. Each challenge will focus on a specific hook, and the final challenge will incorporate multiple hooks. Use this exercise to explore how React hooks can be used together to build dynamic forms and UI elements. + +## Instructions +### Step 1: Download the Project +1. Download the project as a zip file from the [repository](https://github.com/buildingu/React_Lesson_Exercise), **Do not clone** the repository since you're not contributing to it, and extract it to your bu-learning repository with the name `Exercise 7` or any other name you think makes sense. You should already have the bu-learning repository cloned, if not, clone it [here](https://github.com/buildingu/bu-learning.git). +2. Open the exercise directory in your preferred code editor or IDE. + +### Step 2: Set Up the React App +At the same level where the package.json is located install the dependencies in a terminal: +``` +$ npm install +``` + +### Step 3: The Challenges +Each challenge focuses on demonstrating the concept of each hook except the final challenge where you can use however many hooks you want. You will find the challenges in the src/components directory. +- Each file is named according to the hook it demonstrates. +- Read the comments inside each challenge file for what you have to complete. + +Here are the challenges you'll be working on: +1. **Hook Challenges:** + - `1.UseStateChallenge.jsx`: Demonstrate the use of `useState`. + - `2.UseRefChallenge.jsx`: Demonstrate the use of `useRef`. + - `3.UseEffectChallenge.jsx`: Demonstrate the use of `useEffect`. + - `4.useContextChallenge`: A directory to mimic the idea of the context being used somewhere else in the project; containing AuthContext.jsx and UseContext.jsx for demonstrating useContext. + - `5.UseReducerChallenge.jsx`: Demonstrate the use of `useReducer` + - `6.UseMemoChallenge.jsx` (Optional): Show how `useMemo` can optimize performance. +2. **Final Challenge:** + - The `7.FinalChallenge.jsx` will be a form that incorporates multiple hooks to manage form data, validation, and real-time feedback. + +### Step 5: Running the Challenges +The project is set up so you can run each challenge separately using npm scripts. Use the following commands in the terminal to start each challenge (e.g., npm run dev:useState): + +``` +$ npm run dev: +``` + +**Available Scripts:** +- `dev:useState` +- `dev:useRef` +- `dev:useEffect` +- `dev:useContext` +- `dev:useReducer` +- `dev:useMemo` (Optional) +- `dev:final` (Final Challenge) + +## Bonus! +Instead of using npm scripts like I mentioned to navigate between challenges, you can implement client-side routing using [`react-router-dom`](https://reactrouter.com) to create a page for each challenge. This will require modifying `App.jsx` to include route paths for each challenge and use the component as a page. + +If you decide to implement routing, you can delete the individual challenge scripts from the `package.json` and only use the `"dev"` script to run the app. + +## Your Submission +1. Open a terminal in your code editor (e.g., VSCode) or use Git Bash/any other terminal of your choice if not open already. +2. Ensure you are on your branch, it should be named as the first letter of your first name, followed by an underscore, and your last name (e.g., j_doe). If you somehow didn't create your branch yet use `git checkout -b your_branch_name` to create it. +3. Make the necessary changes to the files required in the exercise and stage your changes using `git add ` to add specific files, or use `git add .` to stage all modified files. +4. Commit your files with a message (see commit message convention: [Conventional Commit Message](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13)) using: +``` +$ git commit -m "feat: Insert message here" +``` +5. Push the changes on your branch to the remote repository using: +``` +$ git push origin your-branch-name +``` diff --git a/Exercise 7/index.html b/Exercise 7/index.html new file mode 100644 index 00000000..e3bf1df4 --- /dev/null +++ b/Exercise 7/index.html @@ -0,0 +1,13 @@ + + + + + + + React 18 Exercise + + +
+ + + \ No newline at end of file diff --git a/Exercise 7/package-lock.json b/Exercise 7/package-lock.json new file mode 100644 index 00000000..e3ff9242 --- /dev/null +++ b/Exercise 7/package-lock.json @@ -0,0 +1,1688 @@ +{ + "name": "exercise-7", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "exercise-7", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.0.0", + "cross-env": "^7.0.3", + "vite": "^5.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.151", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", + "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/Exercise 7/package.json b/Exercise 7/package.json new file mode 100644 index 00000000..b11b75e7 --- /dev/null +++ b/Exercise 7/package.json @@ -0,0 +1,31 @@ +{ + "name": "exercise-7", + "version": "1.0.0", + "private": true, + "type": "module", + + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + + "dev:useState": "cross-env VITE_REACT_CHALLENGE=useState vite", + "dev:useRef": "cross-env VITE_REACT_CHALLENGE=useRef vite", + "dev:useEffect": "cross-env VITE_REACT_CHALLENGE=useEffect vite", + "dev:useContext": "cross-env VITE_REACT_CHALLENGE=useContext vite", + "dev:useReducer": "cross-env VITE_REACT_CHALLENGE=useReducer vite", + "dev:useMemo": "cross-env VITE_REACT_CHALLENGE=useMemo vite", + "dev:final": "cross-env VITE_REACT_CHALLENGE=final vite" + }, + + "dependencies": { + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + + "devDependencies": { + "@vitejs/plugin-react": "^4.0.0", + "vite": "^5.0.0", + "cross-env": "^7.0.3" + } +} diff --git a/Exercise 7/src/App.jsx b/Exercise 7/src/App.jsx new file mode 100644 index 00000000..79b5ab6d --- /dev/null +++ b/Exercise 7/src/App.jsx @@ -0,0 +1,45 @@ +// App.jsx is basically used later in the index.jsx file, it allows us to run the .jsx component files. + +import UseStateChallenge from "./components/UseStateChallenge.jsx"; +import UseRefChallenge from "./components/UseRefChallenge.jsx"; +import UseEffectChallenge from "./components/UseEffectChallenge.jsx"; +import UseContextChallenge from "./components/useContextChallenge/UseContext.jsx"; +import UseReducerChallenge from "./components/UseReducerChallenge.jsx"; +import UseMemoChallenge from "./components/UseMemoChallenge.jsx"; +import FinalChallenge from "./components/FinalChallenge.jsx"; +function App() { + // VITE_REACT_CHALLENGE comes from the npm script, allows changing demos without touching code. + // This means I can run any of the challenges by just changing the npm script in package.json + const hookName = import.meta.env.VITE_REACT_CHALLENGE; // set by your npm script; + + // switch statement to determine which challenge to run + // I know this from C#, it's basically if-statements + // but more readable and easier to maintain + switch (hookName) { + // to run any of them, do: "npm run dev:" + // e.g. "npm run dev:useState" will run the useState challenge + case "useState": return ; + case "useRef": return ; + case "useEffect": return ; + case "useContext": return ; + case "useReducer": return ; + case "useMemo": return ; + case "final": return ; + // by default, the app will return an error message + // if the hook name is not recognized + default: + return ( + // color: white. This block is for the error message + // when the hook name is not recognized + // using for code formatting +

+ ERROR: Please specify a valid hook challenge
+ (e.g. npm run dev:useState) +

// use " " for space, had issues with spaces in the past. + ); + } +} + +// export default App; will be used in the index.jsx file +// to render the App component in the root element +export default App; diff --git a/Exercise 7/src/components/FinalChallenge.jsx b/Exercise 7/src/components/FinalChallenge.jsx new file mode 100644 index 00000000..18f24b59 --- /dev/null +++ b/Exercise 7/src/components/FinalChallenge.jsx @@ -0,0 +1,280 @@ +// Import React and several hooks from the React library +import React, { useState, useEffect, useRef, useContext, useReducer } from 'react'; +// Import AuthContext and AuthProvider from a local file for authentication and theme context +import { AuthContext, AuthProvider } from './useContextChallenge/AuthContext'; + +// This is a comment block describing the improvements and features of this component +/** + * Improved FinalChallenge.jsx + * + * Changes made: + * - Moved useContext inside a provider: Wrapped form content with and moved context usage inside to fix the blank screen (user was undefined without provider). + * - Extended useReducer to handle form submission (added a 'SUBMIT' action and 'submitted' state) for submission feedback. + * - Added conditional rendering of a success message upon form submission. + * - Added styling (padding, margins, colors, etc.) to inputs, buttons, and container for a polished, responsive UI. + * - Included comments explaining how each React hook is used. + */ + +// Define the initial state for the form, with empty name and email, and not submitted +const initialFormState = { name: '', email: '', submitted: false, touched: false }; + +// This function manages how the form state changes based on different actions +function formReducer(state, action) { + // Check the type of action being dispatched; which simply means what action is being performed + // and update the state accordingly + switch (action.type) { + case 'CHANGE': + // If the action is 'CHANGE', update the specific field (name or email) and reset 'submitted' to false + return { ...state, [action.field]: action.value, submitted: false }; // .field means the field name is dynamic, it can be either name or email + case 'RESET': + // If the action is 'RESET', return the initial form state (clear all fields and submission status) + return initialFormState; + + case 'SUBMIT': + // If the action is 'SUBMIT', set 'submitted' to true (keep the current field values) + return { ...state, submitted: true }; + + // If the user initially started the website, it won't throw an error + case 'TOUCH': + return { ...state, touched: true }; + default: + // If the action type is not recognized, return the current state unchanged + return state; + } +} + +// Define a styles object to store CSS styles for the component +const styles = { + container: { + // Set the maximum width of the form container + maxWidth: '400px', + // Center the container horizontally and add top margin + margin: '30px auto', + // Add padding inside the container + padding: '20px', + // Round the corners of the container + borderRadius: '8px', + // Add a subtle box shadow for depth + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', + // Set a default background color (can be changed by theme) + backgroundColor: '#ffffff', + // Set a default text color (can be changed by theme) + color: '#343a40', + }, + input: { + // Make input fields take up the full width of the container + width: '100%', + // Add padding inside the input fields + padding: '10px', + // Set the font size for input text + fontSize: '16px', + // Add space below each input + marginBottom: '1rem', + // Round the corners of the input fields + borderRadius: '5px', + // Add a light border around the input fields + border: '1px solid #ced4da', + // Make the input background transparent so the container's background shows through + backgroundColor: 'transparent', + // Make the input text color inherit from the container + color: 'inherit' + }, + button: { + // Add padding inside the buttons + padding: '10px 20px', + // Set the font size for button text + fontSize: '16px', + // Remove the default border from buttons + border: 'none', + // Round the corners of the buttons + borderRadius: '5px', + // Change the cursor to a pointer when hovering over buttons + cursor: 'pointer' + }, + submitButton: { + // Set the background color for the submit button (blue) + backgroundColor: '#007bff', + // Set the text color for the submit button (white) + color: '#ffffff' + }, + resetButton: { + // Set the background color for the reset button (gray) + backgroundColor: '#6c757d', + // Set the text color for the reset button (white) + color: '#ffffff', + // Add space to the left of the reset button + marginLeft: '10px' + }, + errorText: { + // Add space above the error message + marginTop: '10px', + // Set the text color for the error message (red) + color: '#dc3545', + // Set the background color for the error message (light red) + backgroundColor: '#f8d7da', + // Add padding inside the error message + padding: '10px', + // Round the corners of the error message box + borderRadius: '5px' + }, + successText: { + // Add space above the success message + marginTop: '10px', + // Set the text color for the success message (green) + color: '#155724', + // Set the background color for the success message (light green) + backgroundColor: '#d4edda', + // Add padding inside the success message + padding: '10px', + // Round the corners of the success message box + borderRadius: '5px' + }, + validText: { + // Add space above the success message + marginTop: '10px', + // Set the text color for the success message (dark blue) + color: '#004085', + // Set the background color for the success message (light blue) + backgroundColor: '#cce5ff', + // Add padding inside the success message + padding: '10px', + // Round the corners of the success message box + borderRadius: '5px' + } +}; + +// This is the main exported component for the file +export default function FinalChallenge() { + // Wrap the form in the AuthProvider so that any child component can access authentication and theme context + return ( + + {/* Render the actual form inside the AuthProvider */} + + + ); +} + +// This is the inner component that contains the form and uses several React hooks +function FinalChallengeForm() { + // useContext: Get the current user and theme from the AuthContext + const { user, theme } = useContext(AuthContext); + + // useRef: Create a reference to the name input field so we can focus it automatically + const inputRef = useRef(null); + + // useReducer: Manage the form's state (name, email, submitted) using the reducer function and initial state + const [state, dispatch] = useReducer(formReducer, initialFormState); + + // useState: Track whether the form is valid (used to enable/disable the submit button and show validation messages) + const [isValid, setIsValid] = useState(false); + + // useEffect: Whenever the name or email changes, check if the form is valid + useEffect(() => { + // Check if the name field is not empty (after trimming whitespace) + const nameFilled = state.name.trim().length > 0; + // Check if the email field matches a basic email pattern + const emailValid = /\S+@\S+\.\S+/.test(state.email); // "/\S+@\S+\.\S+/" is a regex pattern for basic email validation + // Set isValid to true only if both fields are valid + setIsValid(nameFilled && emailValid); + }, [state.name, state.email]); // Only run this effect when name or email changes + + // useEffect: When the component first mounts, focus the name input field + useEffect(() => { + // If the inputRef is attached to an input element, focus it + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); // Run this effect only once when the component mounts + + // Adjust the container's background and text color based on the current theme (light or dark) + const containerStyle = { + ...styles.container, + backgroundColor: theme === 'dark' ? '#343a40' : '#ffffff', + color: theme === 'dark' ? '#ffffff' : '#343a40' + }; + + // Render the form UI + return ( + // Apply the container styles to the outer div +
+ {/* Display the form title */} +

Final Challenge

+ {/* Show the logged-in user's name from context, or 'Guest' if no user is logged in */} +

Logged in as: { user ? user.name : 'Guest' }

+ + {/* Input field for the user's name */} + { + dispatch({ type: 'CHANGE', field: 'name', value: e.target.value }); + dispatch({ type: 'TOUCH' }); + }} + // Apply the input styles + style={styles.input} + /> + {/* Input field for the user's email */} + dispatch({ type: 'CHANGE', field: 'email', value: e.target.value })} + // Apply the input styles + style={styles.input} + /> + + {/* Submit button for the form */} + + {/* Reset button to clear the form */} + + + {/* If the form is invalid and hasn't been submitted, show a validation error message */} + {!isValid && state.touched && !state.submitted && ( +

+ Please fill out both fields correctly. +

+ )} + + {/* If the form has not been submitted, but has valid credentials */} + {!state.submitted && state.touched && isValid && ( +

+ Valid Credentials! Feel free to submit the form. +

+ )} + + {/* If the form has been submitted successfully, show a thank you message with the user's name and email */} + {state.submitted && ( +

+ Thank you, {state.name}! We will be in touch at {state.email}. +

+ )} +
+ ); +} \ No newline at end of file diff --git a/Exercise 7/src/components/UseEffectChallenge.jsx b/Exercise 7/src/components/UseEffectChallenge.jsx new file mode 100644 index 00000000..16f8b893 --- /dev/null +++ b/Exercise 7/src/components/UseEffectChallenge.jsx @@ -0,0 +1,558 @@ +// Import React and two hooks: useState (for state) and useEffect (for side effects) +import React, { useState, useEffect } from 'react'; + +// This is a multi-part challenge component to demonstrate useEffect in different scenarios + +/** + * Challenge: Create a component that demonstrates useEffect in multiple scenarios + * + * Requirements: + * 1. Implement a window resize tracker that updates when the window size changes + * 2. Create a countdown timer that runs once when component mounts + * 3. Add a data fetching simulation with loading states + * 4. Implement a "typing indicator" that appears when the user types and disappears after 1 second of inactivity + * 5. Create a cleanup demo to show how useEffect's cleanup function works + */ + +// Define the main functional component +const UseEffectChallenge = () => { + // Create a state variable to store the window's width and height + // useState initializes it with the current window size + const [windowSize, setWindowSize] = useState({ + width: window.innerWidth, // current window width + height: window.innerHeight, // current window height + }); + + // State for the countdown timer value (starts at 10) + const [countdown, setCountdown] = useState(10); + // State to track if the countdown is active or not + const [countdownActive, setCountdownActive] = useState(false); + + // State for fetched data (null means no data yet) + const [data, setData] = useState(null); + // State to show if data is loading + const [loading, setLoading] = useState(false); + // State to store any error message from fetching + const [error, setError] = useState(null); + // State to trigger a new fetch (incrementing this value triggers useEffect) + const [fetchTrigger, setFetchTrigger] = useState(0); + + // State for the text input value + const [inputText, setInputText] = useState(''); + // State to show/hide the typing indicator + const [isTyping, setIsTyping] = useState(false); + + // State to show/hide the cleanup demo section + const [showCleanupDemo, setShowCleanupDemo] = useState(true); + // State to count how many times the effect has run + const [effectCount, setEffectCount] = useState(0); + + // useEffect to track window resizing + useEffect(() => { + // Define a function to update the windowSize state when the window is resized + const handleResize = () => { + setWindowSize({ + width: window.innerWidth, // update width + height: window.innerHeight, // update height + }); + }; + + // Add the resize event listener when the component mounts + window.addEventListener('resize', handleResize); + + // Cleanup function: remove the event listener when the component unmounts + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); // Empty array: this effect runs only once when the component mounts + + // useEffect for the countdown timer + useEffect(() => { + // If countdown is not active, do nothing + if (!countdownActive) return; + + // If countdown reaches 0, stop the countdown + if (countdown <= 0) { + setCountdownActive(false); + return; + } + + // Set a timeout to decrease the countdown by 1 after 1 second (1000ms) + const timerId = setTimeout(() => { + setCountdown(countdown - 1); + }, 1000); + + // Cleanup: clear the timeout if the effect runs again or component unmounts + return () => { + clearTimeout(timerId); + }; + }, [countdown, countdownActive]); // Runs whenever countdown or countdownActive changes + + // useEffect for simulating data fetching + useEffect(() => { + // If fetchTrigger is 0, don't fetch (means user hasn't clicked yet) + if (fetchTrigger === 0) return; + + // Set loading state to true, clear previous error and data + setLoading(true); + setError(null); + setData(null); + + // Define an async function to simulate fetching data + const fetchData = async () => { + try { + // Wait 1.5 seconds to simulate network delay + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Create some mock data with random values + const mockData = { + id: Math.floor(Math.random() * 1000), + title: `Item ${Math.floor(Math.random() * 100)}`, + description: `This is a randomly generated description for item ${Math.floor(Math.random() * 100)}`, + timestamp: new Date().toISOString(), + }; + + // Set the data state with the mock data + setData(mockData); + // Set loading to false since we're done + setLoading(false); + } catch (err) { + // If there's an error, set the error state + setError('An error occurred while fetching data'); + setLoading(false); + } + }; + + // Call the fetchData function + fetchData(); + + // No cleanup needed for this effect + }, [fetchTrigger]); // Runs whenever fetchTrigger changes + + // useEffect for the typing indicator + useEffect(() => { + // If the input is empty, hide the typing indicator and exit + if (inputText.length === 0) { + setIsTyping(false); + return; + } + + // Show the typing indicator + setIsTyping(true); + + // Set a timeout to hide the typing indicator after 1 second of inactivity + const typingTimer = setTimeout(() => { + setIsTyping(false); + }, 1000); + + // Cleanup: clear the timeout if the effect runs again or component unmounts + return () => { + clearTimeout(typingTimer); + }; + }, [inputText]); // Runs whenever inputText changes + + // useEffect for the cleanup demo + useEffect(() => { + // If the cleanup demo is hidden, do nothing + if (!showCleanupDemo) return; + + // Increment the effectCount state by 1 + setEffectCount(prev => prev + 1); + + // Set up an interval to increment effectCount every 2 seconds + const interval = setInterval(() => { + setEffectCount(prev => prev + 1); + }, 2000); + + // Log to the console when the interval is set up + console.log('Effect setup - interval created'); + + // Cleanup: clear the interval and log to the console when the effect is cleaned up + return () => { + clearInterval(interval); + console.log('Effect cleanup - interval cleared'); + }; + }, [showCleanupDemo]); // Runs whenever showCleanupDemo changes + + // Handler function to start the countdown timer + const handleStartCountdown = () => { + setCountdown(10); // Reset countdown to 10 + setCountdownActive(true); // Start the countdown + }; + + // Handler function to trigger data fetching + const handleFetchData = () => { + setFetchTrigger(prev => prev + 1); // Increment fetchTrigger to trigger useEffect + }; + + // Handler function for input changes (typing) + const handleInputChange = (e) => { + setInputText(e.target.value); // Update inputText state with the new value + }; + + // Handler to show/hide the cleanup demo and reset the effect count if hiding + const toggleCleanupDemo = () => { + setShowCleanupDemo(prev => !prev); // Toggle showCleanupDemo state + if (showCleanupDemo) { + setEffectCount(0); // Reset effectCount if hiding the demo + } + }; + + // The component's rendered UI + return ( + // Main container div with styling +
+ {/* Title */} +

useEffect Challenge

+ + {/* Window Resize Tracker Section */} +
+

Window Resize Tracker

+
+
+ Width: + {windowSize.width}px +
+
+ Height: + {windowSize.height}px +
+
+

Resize your browser window to see the values update in real-time!

+
+ + {/* Countdown Timer Section */} +
+

Countdown Timer

+
+
+ {/* Show "Time's up!" if countdown is 0, otherwise show the countdown value */} + {countdown === 0 ? ( + Time's up! + ) : ( + {countdown} + )} +
+ +
+
+ + {/* Data Fetching Demo Section */} +
+

Data Fetching with useEffect

+
+ + +
+ {/* Show loader if loading */} + {loading &&
} + + {/* Show error message if there is an error */} + {error &&
{error}
} + + {/* Show fetched data if available and not loading or error */} + {data && !loading && !error && ( +
+

{data.title}

+

{data.description}

+

Fetched at: {data.timestamp}

+
+ )} + + {/* Show message before any fetch */} + {!data && !loading && !error && fetchTrigger === 0 && ( +

Click the button to fetch data

+ )} + + {/* Show message if no data after fetch */} + {!data && !loading && !error && fetchTrigger > 0 && ( +

No data available

+ )} +
+
+
+ + {/* Typing Indicator Section */} +
+

Typing Indicator

+
+ {/* Text input for typing */} + + {/* Show typing indicator if isTyping is true */} + {isTyping && ( +
+ + + + Typing... +
+ )} +
+

+ The typing indicator appears when you type and disappears after 1 second of inactivity. +

+
+ + {/* Cleanup Demo Section */} +
+

useEffect Cleanup Demo

+
+ {/* Button to show/hide the cleanup demo */} + + + {/* Show the cleanup demo if enabled */} + {showCleanupDemo && ( +
+

+ This component has an effect that runs every 2 seconds. +

+

+ Effect has run {effectCount} times. +

+

+ Check the console for cleanup messages when you hide this component. +

+
+ )} +
+
+
+ ); +}; + +// Define a styles object for inline styling +const styles = { + container: { + maxWidth: '800px', // Maximum width of the container + margin: '0 auto', // Center the container horizontally + padding: '20px', // Padding inside the container + backgroundColor: '#f8f9fa', // Light background color + borderRadius: '10px', // Rounded corners + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', // Subtle shadow + fontFamily: 'Arial, sans-serif', // Font family + }, + title: { + textAlign: 'center', // Center the title + color: '#343a40', // Dark gray color + marginBottom: '30px', // Space below the title + fontSize: '28px', // Large font size + }, + section: { + backgroundColor: '#ffffff', // White background for sections + padding: '20px', // Padding inside the section + borderRadius: '8px', // Rounded corners + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', // Light shadow + marginBottom: '25px', // Space below the section + }, + sectionTitle: { + color: '#343a40', // Section title color + borderBottom: '1px solid #e9ecef', // Bottom border + paddingBottom: '10px', // Space below the title + marginTop: '0', // No top margin + }, + description: { + color: '#6c757d', // Muted text color + fontStyle: 'italic', // Italic text + margin: '10px 0', // Vertical margin + }, + windowSizeDisplay: { + display: 'flex', // Use flexbox + justifyContent: 'center', // Center items horizontally + gap: '30px', // Space between items + padding: '20px', // Padding inside + backgroundColor: '#f1f3f5', // Light gray background + borderRadius: '8px', // Rounded corners + }, + sizeItem: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack items vertically + alignItems: 'center', // Center items horizontally + }, + sizeLabel: { + fontSize: '14px', // Small font size + color: '#6c757d', // Muted color + marginBottom: '5px', // Space below label + }, + sizeValue: { + fontSize: '24px', // Large font size + fontWeight: 'bold', // Bold text + color: '#343a40', // Dark color + }, + countdownContainer: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack items vertically + alignItems: 'center', // Center items horizontally + gap: '15px', // Space between items + }, + countdownDisplay: { + width: '100px', // Width of the countdown circle + height: '100px', // Height of the countdown circle + display: 'flex', // Use flexbox + justifyContent: 'center', // Center content horizontally + alignItems: 'center', // Center content vertically + backgroundColor: '#343a40', // Dark background + borderRadius: '50%', // Make it a circle + color: '#ffffff', // White text + }, + countdownValue: { + fontSize: '36px', // Large font size + fontWeight: 'bold', // Bold text + }, + countdownComplete: { + fontSize: '16px', // Medium font size + fontWeight: 'bold', // Bold text + color: '#ffc107', // Yellow color + }, + button: { + padding: '10px 15px', // Padding inside the button + fontSize: '16px', // Font size + backgroundColor: '#007bff', // Blue background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + transition: 'background-color 0.3s', // Smooth background color transition + }, + fetchContainer: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack items vertically + alignItems: 'center', // Center items horizontally + gap: '20px', // Space between items + }, + dataDisplay: { + width: '100%', // Full width + minHeight: '150px', // Minimum height + backgroundColor: '#f8f9fa', // Light background + borderRadius: '8px', // Rounded corners + padding: '15px', // Padding inside + position: 'relative', // For positioning loader + }, + loader: { + width: '40px', // Loader size + height: '40px', + margin: '30px auto', // Center loader + border: '4px solid #f3f3f3', // Light border + borderTop: '4px solid #007bff', // Blue top border for spinner effect + borderRadius: '50%', // Make it a circle + animation: 'spin 1s linear infinite', // Spin animation + }, + '@keyframes spin': { + '0%': { transform: 'rotate(0deg)' }, // Start at 0 degrees + '100%': { transform: 'rotate(360deg)' }, // End at 360 degrees + }, + error: { + color: '#dc3545', // Red color for errors + textAlign: 'center', // Center text + padding: '20px', // Padding + fontWeight: 'bold', // Bold text + }, + noData: { + color: '#6c757d', // Muted color + textAlign: 'center', // Center text + padding: '20px', // Padding + fontStyle: 'italic', // Italic text + }, + dataContent: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack items vertically + gap: '10px', // Space between items + }, + dataTitle: { + color: '#343a40', // Dark color + margin: '0', // No margin + fontSize: '20px', // Medium font size + }, + dataDescription: { + color: '#495057', // Gray color + margin: '0', // No margin + }, + dataTimestamp: { + color: '#6c757d', // Muted color + fontSize: '12px', // Small font + margin: '10px 0 0 0', // Margin above + fontStyle: 'italic', // Italic text + }, + typingContainer: { + position: 'relative', // For positioning typing indicator + marginBottom: '30px', // Space below + }, + input: { + width: '100%', // Full width + padding: '12px', // Padding inside + fontSize: '16px', // Font size + borderRadius: '5px', // Rounded corners + border: '1px solid #ced4da', // Light border + outline: 'none', // No outline + transition: 'border-color 0.3s', // Smooth border color transition + }, + typingIndicator: { + display: 'flex', // Use flexbox + alignItems: 'center', // Center items vertically + gap: '5px', // Space between dots and text + position: 'absolute', // Position below the input + bottom: '-25px', // 25px below the input + left: '10px', // 10px from the left + }, + typingDot: { + width: '8px', // Dot size + height: '8px', + backgroundColor: '#007bff', // Blue color + borderRadius: '50%', // Make it a circle + animation: 'blink 1s infinite', // Blinking animation + }, + typingText: { + color: '#007bff', // Blue color + fontSize: '14px', // Small font + fontStyle: 'italic', // Italic text + }, + cleanupContainer: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack items vertically + alignItems: 'center', // Center items horizontally + gap: '20px', // Space between items + }, + cleanupDemo: { + width: '100%', // Full width + backgroundColor: '#e9ecef', // Light gray background + borderRadius: '8px', // Rounded corners + padding: '15px', // Padding inside + marginTop: '10px', // Space above + }, + cleanupText: { + color: '#495057', // Gray color + margin: '10px 0', // Vertical margin + }, + cleanupCount: { + fontWeight: 'bold', // Bold text + color: '#007bff', // Blue color + }, +}; + +// Export the component so it can be used in other files +export default UseEffectChallenge; \ No newline at end of file diff --git a/Exercise 7/src/components/UseMemoChallenge.jsx b/Exercise 7/src/components/UseMemoChallenge.jsx new file mode 100644 index 00000000..7e3a8452 --- /dev/null +++ b/Exercise 7/src/components/UseMemoChallenge.jsx @@ -0,0 +1,444 @@ +// Import React and hooks for state and effect management +import React, { useState } from 'react'; + + + + +// --- Utility functions --- + +function relu(x) { // ReLU activation function + return x.map(v => Math.max(0, v)); +} +function reluDerivative(x) { // Derivative of ReLU + return x.map(v => (v > 0 ? 1 : 0)); +} +function sigmoid(x) { // Sigmoid activation function + return x.map(v => 1 / (1 + Math.exp(-v))); +} +function sigmoidDerivative(x) { // Derivative of sigmoid + return x.map(v => v * (1 - v)); +} +function matMul(a, b) { // Matrix multiplication + return a.map(row => + b[0].map((_, j) => + row.reduce((sum, val, i) => sum + val * b[i][j], 0) + ) + ); +} +function addBias(mat, bias) { // Add bias vector to each row of matrix + return mat.map(row => row.map((v, i) => v + bias[i])); +} +function transpose(mat) { // Transpose a matrix + return mat[0].map((_, i) => mat.map(row => row[i])); +} +function subtract(a, b) { // Element-wise subtraction of matrices + return a.map((row, i) => row.map((v, j) => v - b[i][j])); +} +function multiply(a, b) { // Element-wise multiplication of matrices + return a.map((row, i) => row.map((v, j) => v * b[i][j])); +} +function scalarMultiply(mat, scalar) { // Multiply matrix by scalar + return mat.map(row => row.map(v => v * scalar)); +} +function randomMatrix(rows, cols) { // Generate random matrix + return Array.from({ length: rows }, () => + Array.from({ length: cols }, () => Math.random() * 2 - 1) + ); +} +function zeroMatrix(rows, cols) { // Generate zero matrix + return Array.from({ length: rows }, () => + Array.from({ length: cols }, () => 0) + ); +} + +// --- Dataset: XOR problem --- +const DATASET = [ + { input: [0, 0], target: [0] }, // Input/target pairs for XOR + { input: [0, 1], target: [1] }, + { input: [1, 0], target: [1] }, + { input: [1, 1], target: [0] } +]; + +// --- Main React component --- +export default function UseMemoChallenge() { + // State: weights and biases for each layer + const [W1, setW1] = useState(randomMatrix(2, 4)); // Weights for layer 1 + const [b1, setB1] = useState(Array(4).fill(0)); // Biases for layer 1 + const [W2, setW2] = useState(randomMatrix(4, 4)); // Weights for layer 2 + const [b2, setB2] = useState(Array(4).fill(0)); // Biases for layer 2 + const [W3, setW3] = useState(randomMatrix(4, 4)); // Weights for layer 3 + const [b3, setB3] = useState(Array(4).fill(0)); // Biases for layer 3 + const [W4, setW4] = useState(randomMatrix(4, 1)); // Weights for output layer + const [b4, setB4] = useState(Array(1).fill(0)); // Biases for output layer + + // State: activations, loss, gradients, and UI step + const [activations, setActivations] = useState({}); // Stores activations for all layers + const [loss, setLoss] = useState(null); // Stores current loss + const [grads, setGrads] = useState({}); // Stores gradients for all layers + const [step, setStep] = useState(''); // UI step indicator + const [epoch, setEpoch] = useState(0); // Track epoch count + + // --- Forward pass: compute activations and loss --- + function forwardPass(weights = { W1, b1, W2, b2, W3, b3, W4, b4 }) { + // Prepare input as a matrix (batch size = 4) + const X = DATASET.map(d => d.input); + // Layer 1: input -> hidden1 (ReLU) + const z1 = addBias(matMul(X, weights.W1), weights.b1); + const a1 = relu(z1.flat()).reduce((acc, v, i) => { + if (i % 4 === 0) acc.push([]); + acc[acc.length - 1].push(v); + return acc; + }, []); + // Layer 2: hidden1 -> hidden2 (ReLU) + const z2 = addBias(matMul(a1, weights.W2), weights.b2); + const a2 = relu(z2.flat()).reduce((acc, v, i) => { + if (i % 4 === 0) acc.push([]); + acc[acc.length - 1].push(v); + return acc; + }, []); + // Layer 3: hidden2 -> hidden3 (ReLU) + const z3 = addBias(matMul(a2, weights.W3), weights.b3); + const a3 = relu(z3.flat()).reduce((acc, v, i) => { + if (i % 4 === 0) acc.push([]); + acc[acc.length - 1].push(v); + return acc; + }, []); + // Output layer: hidden3 -> output (Sigmoid) + const z4 = addBias(matMul(a3, weights.W4), weights.b4); + const a4 = sigmoid(z4.flat()).map(v => [v]); + // Compute loss (mean squared error) + const Y = DATASET.map(d => d.target); + const mse = a4.reduce((sum, row, i) => sum + Math.pow(row[0] - Y[i][0], 2), 0) / 4; + // Store activations and loss in state + setActivations({ X, z1, a1, z2, a2, z3, a3, z4, a4, Y }); + setLoss(mse); + return { X, z1, a1, z2, a2, z3, a3, z4, a4, Y, mse }; + } + + // --- Button handlers --- + function handleForwardPass() { // Run forward pass + forwardPass(); + setStep('forward'); + setGrads({}); + } + + function handleBackprop() { // Run backpropagation + // Use stored activations from last forward pass + if (!activations.a4) return; + const { X, z1, a1, z2, a2, z3, a3, z4, a4, Y } = activations; + // Output layer error + const dz4 = a4.map((row, i) => [2 * (row[0] - Y[i][0]) * sigmoidDerivative([row[0]])[0]]); + const dW4 = matMul(transpose(a3), dz4); + const db4 = dz4.reduce((acc, row) => acc.map((v, i) => v + row[i]), Array(1).fill(0)); + // Hidden3 layer + const da3 = matMul(dz4, transpose(W4)); + const dz3 = multiply(da3, reluDerivative(z3.flat()).map(v => [v]).reduce((acc, v, i) => { + if (i % 4 === 0) acc.push([]); + acc[acc.length - 1].push(v[0]); + return acc; + }, [])); + const dW3 = matMul(transpose(a2), dz3); + const db3 = dz3.reduce((acc, row) => acc.map((v, i) => v + row[i]), Array(4).fill(0)); + // Hidden2 layer + const da2 = matMul(dz3, transpose(W3)); + const dz2 = multiply(da2, reluDerivative(z2.flat()).map(v => [v]).reduce((acc, v, i) => { + if (i % 4 === 0) acc.push([]); + acc[acc.length - 1].push(v[0]); + return acc; + }, [])); + const dW2 = matMul(transpose(a1), dz2); + const db2 = dz2.reduce((acc, row) => acc.map((v, i) => v + row[i]), Array(4).fill(0)); + // Hidden1 layer + const da1 = matMul(dz2, transpose(W2)); + const dz1 = multiply(da1, reluDerivative(z1.flat()).map(v => [v]).reduce((acc, v, i) => { + if (i % 4 === 0) acc.push([]); + acc[acc.length - 1].push(v[0]); + return acc; + }, [])); + const dW1 = matMul(transpose(X), dz1); + const db1 = dz1.reduce((acc, row) => acc.map((v, i) => v + row[i]), Array(4).fill(0)); + setGrads({ dW1, db1, dW2, db2, dW3, db3, dW4, db4 }); + setStep('backprop'); + } + + function handleGradientDescent() { // Run one step of gradient descent + if (!grads.dW1) return; + const lr = 0.1; // Learning rate + // Update weights and biases for each layer + const newW1 = W1.map((row, i) => row.map((v, j) => v - lr * grads.dW1[i][j])); + const newB1 = b1.map((v, i) => v - lr * grads.db1[i] / 4); + const newW2 = W2.map((row, i) => row.map((v, j) => v - lr * grads.dW2[i][j])); + const newB2 = b2.map((v, i) => v - lr * grads.db2[i] / 4); + const newW3 = W3.map((row, i) => row.map((v, j) => v - lr * grads.dW3[i][j])); + const newB3 = b3.map((v, i) => v - lr * grads.db3[i] / 4); + const newW4 = W4.map((row, i) => row.map((v, j) => v - lr * grads.dW4[i][j])); + const newB4 = b4.map((v, i) => v - lr * grads.db4[i] / 4); + setW1(newW1); + setB1(newB1); + setW2(newW2); + setB2(newB2); + setW3(newW3); + setB3(newB3); + setW4(newW4); + setB4(newB4); + // Immediately run forward pass with new weights to update loss/activations + setTimeout(() => { + forwardPass({ W1: newW1, b1: newB1, W2: newW2, b2: newB2, W3: newW3, b3: newB3, W4: newW4, b4: newB4 }); + setStep('descent'); + }, 0); + } + + // --- Epoch handler: run multiple gradient descent steps --- + function handleEpoch() { + let count = 10; // Number of epochs per click + function runEpoch(i) { + if (i === 0) { + setEpoch(e => e + count); + return; + } + handleBackprop(); + setTimeout(() => { + handleGradientDescent(); + setTimeout(() => runEpoch(i - 1), 10); + }, 10); + } + runEpoch(count); + } + + // --- Helper: pretty print a matrix --- + function printMatrix(mat, decimals = 3) { + return ( +
+        {mat.map(row => row.map(v => v.toFixed(decimals)).join('\t')).join('\n')}
+      
+ ); + } + + // --- Helper: color for node activation --- + function nodeColor(val) { + // Blue for low, pink for high + const c = Math.round(200 + 55 * val); + return `rgb(${c},${180 - 80 * val},${236 - 100 * val})`; + } + + // --- Neural Network Diagram (SVG) --- + function NeuralNetDiagram() { + // Use activations for node values, fallback to zeros + const { X = [[0,0],[0,0],[0,0],[0,0]], a1 = zeroMatrix(4,4), a2 = zeroMatrix(4,4), a3 = zeroMatrix(4,4), a4 = zeroMatrix(4,1) } = activations; + // Layout: 2 input, 4x3 hidden, 1 output + const layers = [ + X, a1, a2, a3, a4 + ]; + const nodeRadius = 16; + const layerGap = 90; + const nodeGap = 44; + const svgWidth = 500; + const svgHeight = 260; + // For each layer, draw nodes and lines + return ( + + {/* Draw connections */} + {layers.slice(0, -1).map((layer, li) => + layer.map((row, i) => + layers[li + 1].map((_, j) => ( + + )) + ) + )} + {/* Draw nodes */} + {layers.map((layer, li) => + layer.map((row, i) => { + // For output, row is [val], for others, row is array + const val = Array.isArray(row) ? (li === 0 ? row[0] : row[i % row.length]) : row; + return ( + + + + {val !== undefined ? val.toFixed(2) : '0.00'} + + + ); + }) + )} + {/* Layer labels */} + Input + Hidden 1 + Hidden 2 + Hidden 3 + Output + + ); + } + + // --- UI: render the neural network simulator --- + return ( +
+ {/* Inline CSS for button hover and focus */} + + {/* Title */} +

Neural Network Simulator

+ {/* Subtitle */} +
+ A simple 3-hidden-layer neural net (ReLU, Sigmoid) trained on XOR. +
+ + Each button runs a step. See activations, loss, and weights below.
+ Tip: Hover buttons for explanations. +
+
+ {/* Neural Network Diagram */} + + {/* Buttons for each process */} +
+ + + + +
+ {/* Show current loss */} + {loss !== null && ( +
+ Loss (MSE): {loss.toFixed(5)} + Epoch: {epoch} +
+ )} + {/* Show activations if available */} + {activations.a4 && ( +
+ Activations (output): + {printMatrix(activations.a4, 4)} +
+ )} + {/* Show gradients if available */} + {step === 'backprop' && grads.dW1 && ( +
+ Sample Gradients (dW1): + {printMatrix(grads.dW1, 4)} +
+ )} + {/* Show weights if just updated */} + {step === 'descent' && ( +
+ Weights (W1, first 2 rows): + {printMatrix(W1.slice(0, 2), 4)} +
+ )} + {/* Responsive note */} +
+ Dataset: XOR (inputs: [0,0], [0,1], [1,0], [1,1])
+ Output: 1 if inputs differ, else 0.
+ Try: Run Forward Pass, then Backpropagation, then Gradient Descent repeatedly.
+ All math and code is manual, no ML libraries used. +
+
+ ); +} \ No newline at end of file diff --git a/Exercise 7/src/components/UseReducerChallenge.jsx b/Exercise 7/src/components/UseReducerChallenge.jsx new file mode 100644 index 00000000..18951985 --- /dev/null +++ b/Exercise 7/src/components/UseReducerChallenge.jsx @@ -0,0 +1,295 @@ +// Import React and the useReducer hook from the React library +import React, { useReducer } from 'react'; + +/** + * Challenge: Create a shopping cart using useReducer + * + * Requirements: + * 1. Add, remove, update quantity, and clear cart functionality + * 2. Use action types & reducer logic + * 3. Display cart items, total count & total price + * 4. Style it so it actually looks like a mini‐shop + */ + +// Define the initial state for the shopping cart +const initialState = { + items: [], // Start with an empty array of items in the cart + totalItems: 0, // Start with zero total items + totalPrice: 0, // Start with a total price of zero +}; + +// Define action types as constants to avoid typos and make code easier to manage +const ACTIONS = { + ADD_ITEM: 'add_item', // Action for adding an item to the cart + REMOVE_ITEM: 'remove_item', // Action for removing an item from the cart + UPDATE_QUANTITY: 'update_quantity', // Action for updating the quantity of an item + CLEAR_CART: 'clear_cart', // Action for clearing the entire cart +}; + +// Create a list of products that can be added to the cart +const products = [ + { id: 1, name: 'React Cookbook', price: 29.99 }, // Product 1 + { id: 2, name: 'JS T-Shirt', price: 19.99 }, // Product 2 + { id: 3, name: 'Node Sticker Pack', price: 4.99 }, // Product 3 +]; + +// Define the reducer function that will handle all cart actions +function cartReducer(state, action) { + // Use a switch statement to handle different action types + switch (action.type) { + case ACTIONS.ADD_ITEM: { // If the action is to add an item + const item = action.payload; // Get the item to add from the action payload + // Check if the item already exists in the cart + const exists = state.items.find(i => i.id === item.id); + + let newItems; // Will hold the new array of items + if (exists) { + // If the item exists, increase its quantity by 1 + newItems = state.items.map(i => + i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i + ); + } else { + // If the item does not exist, add it with quantity 1 + newItems = [...state.items, { ...item, quantity: 1 }]; + } + + // Return the new state with updated items, totalItems, and totalPrice + return { + items: newItems, + totalItems: state.totalItems + 1, // Increase total items by 1 + totalPrice: +(state.totalPrice + item.price).toFixed(2), // Add item's price to total, rounded to 2 decimals + }; + } + + case ACTIONS.REMOVE_ITEM: { // If the action is to remove an item + const id = action.payload; // Get the id of the item to remove + // Find the item to remove in the cart + const toRemove = state.items.find(i => i.id === id); + if (!toRemove) return state; // If item not found, return current state + + // Filter out the item to remove from the items array + const filtered = state.items.filter(i => i.id !== id); + // Return the new state with updated items, totalItems, and totalPrice + return { + items: filtered, + totalItems: state.totalItems - toRemove.quantity, // Subtract the quantity of removed item from total + totalPrice: +( + state.totalPrice - + toRemove.price * toRemove.quantity // Subtract the total price of removed item(s) + ).toFixed(2), + }; + } + + case ACTIONS.UPDATE_QUANTITY: { // If the action is to update quantity + const { id, quantity } = action.payload; // Get id and new quantity from payload + if (quantity < 1) return state; // If quantity is less than 1, do nothing + + // Map through items and update the quantity for the matching item + const updated = state.items.map(i => + i.id === id ? { ...i, quantity } : i + ); + // Calculate the new total number of items + const totalItems = updated.reduce((sum, i) => sum + i.quantity, 0); + // Calculate the new total price + const totalPrice = updated + .reduce((sum, i) => sum + i.price * i.quantity, 0) + .toFixed(2); + + // Return the new state with updated items, totalItems, and totalPrice + return { + items: updated, + totalItems, + totalPrice: +totalPrice, // Convert string to number + }; + } + + case ACTIONS.CLEAR_CART: // If the action is to clear the cart + return initialState; // Reset state to initial values + + default: // If action type is not recognized + return state; // Return current state unchanged + } +} + +// Component to display a single product and an "Add" button +function ProductItem({ product, dispatch }) { + // Render a product card with name, price, and add button + return ( +
+
{product.name}
{/* Show product name */} +
${product.price.toFixed(2)}
{/* Show product price with 2 decimals */} + +
+ ); +} + +// Component to display a single cart item with quantity controls and remove button +function CartItem({ item, dispatch }) { + // Render a cart row with name, quantity controls, price, and remove button + return ( +
+
{item.name}
{/* Show item name */} +
+ {/* Button to decrease quantity */} + + {/* Show current quantity */} + {item.quantity} + {/* Button to increase quantity */} + +
+ {/* Show total price for this item (price * quantity) */} +
${(item.price * item.quantity).toFixed(2)}
+ {/* Button to remove this item from the cart */} + +
+ ); +} + +// Main component that brings everything together +export default function UseReducerChallenge() { + // useReducer returns the current state and a dispatch function to send actions + const [state, dispatch] = useReducer(cartReducer, initialState); + + // Render the shopping cart UI + return ( +
+ {/* Title for the shopping cart */} +

🛒 useReducer Shopping Cart

+ + {/* Section to display available products */} +
+

Products

+ {/* Map through products and render a ProductItem for each */} + {products.map((p) => ( + + ))} +
+ + {/* Section to display the cart */} +
+ {/* Show cart summary: number of items and total price */} +

Cart ({state.totalItems} items) — ${state.totalPrice.toFixed(2)}

+ + {/* If cart is empty, show a message. Otherwise, show cart items */} + {state.items.length === 0 ? ( +

Your cart is empty

+ ) : ( + state.items.map((item) => ( + + )) + )} + + {/* If there are items in the cart, show a "Clear Cart" button */} + {state.items.length > 0 && ( + + )} +
+
+ ); +} + +// Define styles for the components as a JavaScript object +const styles = { + container: { + fontFamily: 'sans-serif', // Use a sans-serif font for the whole app + padding: '20px', // Add padding around the container + maxWidth: '600px', // Limit the width of the container + margin: 'auto', // Center the container horizontally + }, + products: { + display: 'flex', // Arrange products in a row + gap: '10px', // Add space between product cards + marginBottom: '30px', // Add space below the products section + flexWrap: 'wrap', // Allow products to wrap to the next line + }, + productItem: { + border: '1px solid #ddd', // Light border around each product + borderRadius: '5px', // Rounded corners + padding: '10px', // Padding inside the product card + width: '150px', // Fixed width for each product card + textAlign: 'center', // Center text inside the card + }, + addButton: { + marginTop: '10px', // Space above the add button + cursor: 'pointer', // Show pointer cursor on hover + }, + cart: { + borderTop: '2px solid #333', // Dark border above the cart section + paddingTop: '20px', // Space above the cart content + }, + cartItem: { + display: 'flex', // Arrange cart item content in a row + justifyContent: 'space-between', // Space out the content + alignItems: 'center', // Vertically center the content + borderBottom: '1px solid #eee', // Light border below each cart item + padding: '8px 0', // Vertical padding for each cart item + }, + cartName: { + flex: 1, // Allow the name to take up available space + }, + qtyButton: { + margin: '0 5px', // Space on left and right of quantity buttons + cursor: 'pointer', // Show pointer cursor on hover + }, + qty: { + minWidth: '20px', // Minimum width for the quantity display + textAlign: 'center', // Center the quantity text + }, + removeButton: { + cursor: 'pointer', // Show pointer cursor on hover + color: 'red', // Red color for the remove button + border: 'none', // No border for the button + background: 'none', // No background for the button + }, + clearButton: { + marginTop: '15px', // Space above the clear button + padding: '8px 12px', // Padding inside the button + cursor: 'pointer', // Show pointer cursor on hover + }, + empty: { + fontStyle: 'italic', // Italic text for empty cart message + color: '#777', // Gray color for the message + }, +}; \ No newline at end of file diff --git a/Exercise 7/src/components/UseRefChallenge.jsx b/Exercise 7/src/components/UseRefChallenge.jsx new file mode 100644 index 00000000..84709ce0 --- /dev/null +++ b/Exercise 7/src/components/UseRefChallenge.jsx @@ -0,0 +1,391 @@ +// Import React and some useful hooks from the 'react' library +import React, { useRef, useState, useEffect } from 'react'; + +// This is a React functional component called UseRefChallenge +const UseRefChallenge = () => { + // Create a reference to the input element so we can focus it later + const inputRef = useRef(null); + + // Create a reference to store the interval ID for the stopwatch timer + const timerIdRef = useRef(null); + + // Create a reference to store the previous time value for the stopwatch + const prevTimeRef = useRef(0); + + // Create a reference to the box element for detecting clicks outside of it + const boxRef = useRef(null); + + // Create a state variable to keep track of the elapsed time in milliseconds + const [time, setTime] = useState(0); + + // Create a state variable to know if the stopwatch is running or not + const [isRunning, setIsRunning] = useState(false); + + // Create a state variable to count how many times the user has clicked outside the box + const [outsideClicks, setOutsideClicks] = useState(0); + + // Create a state variable to store the current value of the input field + const [inputValue, setInputValue] = useState(''); + + // Create a state variable to store all the values the user has saved from the input + const [savedValues, setSavedValues] = useState([]); + + // This useEffect runs once when the component mounts (because of the empty array []) + useEffect(() => { + // Focus the input element as soon as the component appears on the page + inputRef.current.focus(); + }, []); + + // This useEffect sets up and cleans up the click outside detector + useEffect(() => { + // This function will be called whenever the user clicks anywhere on the page + const handleClickOutside = (event) => { + // If the boxRef exists and the clicked element is NOT inside the box + if (boxRef.current && !boxRef.current.contains(event.target)) { + // Increase the outsideClicks counter by 1 + setOutsideClicks(prev => prev + 1); + } + }; + + // Add an event listener to the whole document for mouse down events + document.addEventListener('mousedown', handleClickOutside); + + // Cleanup function: remove the event listener when the component unmounts + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + // This function starts the stopwatch timer + const startTimer = () => { + // Only start if the timer isn't already running + if (!isRunning) { + // Set the running state to true + setIsRunning(true); + // Calculate the starting point for the timer (handles resume after pause) + prevTimeRef.current = Date.now() - time; + // Start a new interval that updates the time every 10 milliseconds + timerIdRef.current = setInterval(() => { + // Update the time state with the elapsed time + setTime(Date.now() - prevTimeRef.current); + }, 10); + } + }; + + // This function stops (pauses) the stopwatch timer + const stopTimer = () => { + // Only stop if the timer is currently running + if (isRunning) { + // Clear the interval so the timer stops updating + clearInterval(timerIdRef.current); + // Set the running state to false + setIsRunning(false); + } + }; + + // This function resets the stopwatch timer to zero + const resetTimer = () => { + // Clear the interval in case it's running + clearInterval(timerIdRef.current); + // Set the running state to false + setIsRunning(false); + // Reset the time state to zero + setTime(0); + }; + + // This function formats the time in mm:ss:ms for display + const formatTime = () => { + // Calculate the number of minutes + const minutes = Math.floor(time / 60000); + // Calculate the number of seconds (after removing minutes) + const seconds = Math.floor((time % 60000) / 1000); + // Calculate the number of hundredths of a second (after removing seconds) + const milliseconds = Math.floor((time % 1000) / 10); + + // Return a string in the format "mm:ss:ms", always two digits each + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${milliseconds.toString().padStart(2, '0')}`; + }; + + // This function updates the inputValue state when the user types in the input + const handleInputChange = (e) => { + // Set the inputValue state to whatever the user typed + setInputValue(e.target.value); + }; + + // This function saves the current input value to the savedValues array + const handleSaveValue = () => { + // Only save if the input isn't just empty spaces + if (inputValue.trim()) { + // Add the current inputValue to the end of the savedValues array + setSavedValues(prev => [...prev, inputValue]); + // Clear the input field + setInputValue(''); + // Focus the input again so the user can keep typing + inputRef.current.focus(); + } + }; + + // This is the JSX that defines what the component looks like on the page + return ( + // The main container div with some styling +
+ {/* The main title of the page */} +

useRef Challenge

+ + {/* Section for the auto-focus input */} +
+ {/* Section title */} +

Auto-Focus Input

+ {/* Container for the input and save button */} +
+ {/* The input field, connected to inputRef and inputValue */} + + {/* Button to save the current input value */} + +
+ + {/* If there are any saved values, show them in a list */} + {savedValues.length > 0 && ( +
+ {/* Title for the saved values list */} +

Saved Values:

+ {/* List of saved values */} +
    + {/* Map over each saved value and display it in a list item */} + {savedValues.map((value, index) => ( +
  • + {value} +
  • + ))} +
+
+ )} +
+ + {/* Section for the stopwatch */} +
+ {/* Section title */} +

Stopwatch using useRef

+ {/* Container for the stopwatch display and buttons */} +
+ {/* Display the formatted time */} +
{formatTime()}
+ {/* Group of buttons for controlling the stopwatch */} +
+ {/* Start button, only enabled if not running */} + + {/* Stop button, only enabled if running */} + + {/* Reset button, always enabled */} + +
+
+
+ + {/* Section for the click outside detector */} +
+ {/* Section title */} +

Click Outside Detector

+ {/* Description for the user */} +

+ Click anywhere outside the box to increase the counter. +

+ {/* Container for the box and the counter */} +
+ {/* The box we're tracking clicks outside of */} +
+

I'm tracking clicks outside me!

+
+ {/* Display the number of outside clicks */} +
+ Outside Clicks: {outsideClicks} +
+
+
+
+ ); +}; + +// This object contains all the styles used in the component +const styles = { + // Style for the main container + container: { + maxWidth: '800px', // Maximum width of the container + margin: '0 auto', // Center the container horizontally + padding: '20px', // Padding inside the container + backgroundColor: '#f8f9fa', // Light gray background + borderRadius: '10px', // Rounded corners + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', // Subtle shadow + fontFamily: 'Arial, sans-serif', // Font for all text + }, + // Style for the main title + title: { + textAlign: 'center', // Center the text + color: '#343a40', // Dark gray color + marginBottom: '30px', // Space below the title + fontSize: '28px', // Large font size + }, + // Style for each section + section: { + backgroundColor: '#ffffff', // White background + padding: '20px', // Padding inside the section + borderRadius: '8px', // Rounded corners + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.05)', // Light shadow + marginBottom: '25px', // Space below the section + }, + // Style for section titles + sectionTitle: { + color: '#343a40', // Dark gray color + borderBottom: '1px solid #e9ecef', // Light border below the title + paddingBottom: '10px', // Space below the title text + marginTop: '0', // No space above the title + }, + // Style for the input and button container + inputContainer: { + display: 'flex', // Arrange children in a row + gap: '10px', // Space between input and button + marginBottom: '15px', // Space below the container + }, + // Style for the input field + input: { + flex: '1', // Take up remaining space + padding: '12px', // Padding inside the input + fontSize: '16px', // Font size + borderRadius: '5px', // Rounded corners + border: '1px solid #ced4da', // Light border + outline: 'none', // No outline when focused + transition: 'border-color 0.3s', // Smooth border color change + }, + // Style for all buttons + button: { + padding: '10px 15px', // Padding inside the button + fontSize: '16px', // Font size + backgroundColor: '#007bff', // Blue background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + transition: 'background-color 0.3s', // Smooth color change + }, + // Style for the container of saved values + savedValuesContainer: { + marginTop: '15px', // Space above the container + }, + // Style for the saved values title + savedValuesTitle: { + color: '#495057', // Medium gray color + margin: '10px 0', // Space above and below the title + }, + // Style for the list of saved values + savedValuesList: { + backgroundColor: '#f8f9fa', // Light gray background + borderRadius: '5px', // Rounded corners + padding: '10px', // Padding inside the list + margin: '0', // No margin + }, + // Style for each saved value item + savedValueItem: { + padding: '8px 0', // Space above and below each item + borderBottom: '1px solid #e9ecef', // Light border below each item + listStyleType: 'none', // No bullet points + }, + // Style for the stopwatch container + stopwatchContainer: { + display: 'flex', // Arrange children in a column + flexDirection: 'column', // Vertical layout + alignItems: 'center', // Center horizontally + }, + // Style for the time display + timeDisplay: { + fontSize: '36px', // Large font size + fontFamily: 'monospace', // Monospace font for numbers + backgroundColor: '#343a40', // Dark background + color: '#ffffff', // White text + padding: '15px 30px', // Padding inside the display + borderRadius: '8px', // Rounded corners + marginBottom: '20px', // Space below the display + width: '200px', // Fixed width + textAlign: 'center', // Center the text + }, + // Style for the group of stopwatch buttons + buttonGroup: { + display: 'flex', // Arrange buttons in a row + gap: '15px', // Space between buttons + }, + // Style for the description text + description: { + color: '#6c757d', // Gray color + marginBottom: '15px', // Space below the description + fontStyle: 'italic', // Italic text + }, + // Style for the detector container + detectorContainer: { + display: 'flex', // Arrange children in a column + flexDirection: 'column', // Vertical layout + alignItems: 'center', // Center horizontally + gap: '20px', // Space between children + }, + // Style for the box that detects outside clicks + box: { + width: '250px', // Fixed width + height: '150px', // Fixed height + backgroundColor: '#007bff', // Blue background + color: '#ffffff', // White text + display: 'flex', // Center content + justifyContent: 'center', // Center horizontally + alignItems: 'center', // Center vertically + borderRadius: '8px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + transition: 'transform 0.3s, box-shadow 0.3s', // Smooth transitions + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)', // Shadow for depth + }, + // Style for the text inside the box + boxText: { + textAlign: 'center', // Center the text + padding: '10px', // Padding inside the text + fontSize: '18px', // Font size + fontWeight: 'bold', // Bold text + }, + // Style for the outside clicks counter + clicksCounter: { + fontSize: '18px', // Font size + padding: '10px 15px', // Padding inside the counter + backgroundColor: '#e9ecef', // Light gray background + borderRadius: '5px', // Rounded corners + color: '#343a40', // Dark gray text + }, + // Style for the number in the outside clicks counter + clicksValue: { + fontWeight: 'bold', // Bold number + color: '#dc3545', // Red color + }, +}; + +// Export the UseRefChallenge component so it can be used in other files +export default UseRefChallenge; \ No newline at end of file diff --git a/Exercise 7/src/components/UseStateChallenge.jsx b/Exercise 7/src/components/UseStateChallenge.jsx new file mode 100644 index 00000000..dc19e849 --- /dev/null +++ b/Exercise 7/src/components/UseStateChallenge.jsx @@ -0,0 +1,301 @@ +// Import React and the useState hook from the 'react' library +import React, { useState } from 'react'; + +/** + * Challenge: Create a counter component using useState + * + * Requirements: + * 1. Implement a counter with increment, decrement, and reset functionality + * 2. Add a count history feature that tracks previous counter values + * 3. Allow setting the counter to a specific value using an input field + * 4. Add some styling to make it visually appealing + */ + +// Define a functional React component called UseStateChallenge +const UseStateChallenge = () => { + // Declare a state variable 'count' to store the current counter value, and 'setCount' to update it. Start at 0. + const [count, setCount] = useState(0); + + // Declare a state variable 'countHistory' to store an array of previous counter values, and 'setCountHistory' to update it. Start as an empty array. + const [countHistory, setCountHistory] = useState([]); + + // Declare a state variable 'inputValue' to store the value from the input field, and 'setInputValue' to update it. Start as an empty string. + const [inputValue, setInputValue] = useState(''); + + // Define a function to increment the counter by 1 + const handleIncrement = () => { + // Use the previous count value to calculate the new count + setCount(prevCount => { + // Add 1 to the previous count + const newCount = prevCount + 1; + // Add the new count to the history array + setCountHistory(prevHistory => [...prevHistory, newCount]); + // Return the new count so React updates the state + return newCount; + }); + }; + + // Define a function to decrement the counter by 1 + const handleDecrement = () => { + // Use the previous count value to calculate the new count + setCount(prevCount => { + // Subtract 1 from the previous count + const newCount = prevCount - 1; + // Add the new count to the history array + setCountHistory(prevHistory => [...prevHistory, newCount]); + // Return the new count so React updates the state + return newCount; + }); + }; + + // Define a function to reset the counter to 0 + const handleReset = () => { + // Set the counter value to 0 + setCount(0); + // Add 0 to the history array + setCountHistory(prevHistory => [...prevHistory, 0]); + }; + + // Define a function to handle changes in the input field + const handleInputChange = (e) => { + // Update the inputValue state with the new value from the input field + setInputValue(e.target.value); + }; + + // Define a function to set the counter to the value entered in the input field + const handleSetCounterValue = () => { + // Convert the input value from a string to an integer + const parsedValue = parseInt(inputValue, 10); + // Check if the parsed value is a valid number (not NaN) + if (!isNaN(parsedValue)) { + // Set the counter to the parsed value + setCount(parsedValue); + // Add the new value to the history array + setCountHistory(prevHistory => [...prevHistory, parsedValue]); + // Clear the input field + setInputValue(''); + } + }; + + // Define a function to clear the count history + const handleClearHistory = () => { + // Set the history array to an empty array + setCountHistory([]); + }; + + // The component returns JSX to render the UI + return ( + // Main container div with a class and inline styles +
+ {/* Title of the counter app */} +

useState Counter Challenge

+ + {/* Display the current counter value */} +
+

{count}

+
+ + {/* Container for the increment, decrement, and reset buttons */} +
+ {/* Button to decrement the counter */} + + {/* Button to reset the counter */} + + {/* Button to increment the counter */} + +
+ + {/* Container for the input field and set value button */} +
+ {/* Input field to enter a specific counter value */} + + {/* Button to set the counter to the input value */} + +
+ + {/* Container for the count history section */} +
+ {/* Header for the history section, includes title and clear button */} +
+ {/* Title for the history section */} +

Count History

+ {/* Button to clear the history */} + +
+ + {/* List of previous counter values */} +
+ {/* If there is no history, show a message */} + {countHistory.length === 0 ? ( +

No history yet

+ ) : ( + // Otherwise, map over the history array and display each value + countHistory.map((value, index) => ( + // Each history item is shown in a styled div +
+ {/* Show the index (starting at 1) and the value */} + {index + 1}. Changed to {value} +
+ )) + )} +
+
+
+ ); +}; + +// Define a 'styles' object to store all the inline styles for the component +const styles = { + // Styles for the main container + container: { + maxWidth: '600px', // Maximum width of the container + margin: '0 auto', // Center the container horizontally + padding: '20px', // Padding inside the container + backgroundColor: '#f8f9fa', // Light background color + borderRadius: '10px', // Rounded corners + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', // Subtle shadow for depth + fontFamily: 'Arial, sans-serif', // Font for the text + }, + // Styles for the title + title: { + textAlign: 'center', // Center the text + color: '#343a40', // Dark gray color + marginBottom: '20px', // Space below the title + }, + // Styles for the counter display area + counterDisplay: { + backgroundColor: '#ffffff', // White background + padding: '20px', // Padding inside the box + borderRadius: '8px', // Rounded corners + boxShadow: 'inset 0 2px 4px rgba(0, 0, 0, 0.1)', // Inner shadow for effect + marginBottom: '20px', // Space below the counter + }, + // Styles for the counter value + counterValue: { + textAlign: 'center', // Center the number + fontSize: '48px', // Large font size + margin: '0', // No margin + color: '#007bff', // Blue color + }, + // Styles for the button container + buttonContainer: { + display: 'flex', // Use flexbox layout + justifyContent: 'space-between', // Space buttons evenly + marginBottom: '20px', // Space below the buttons + }, + // Styles for the increment, decrement, and reset buttons + button: { + padding: '10px 20px', // Padding inside the button + fontSize: '16px', // Font size + backgroundColor: '#007bff', // Blue background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + transition: 'all 0.3s ease', // Smooth transition for hover effects + }, + // Styles for the input and set value button container + inputContainer: { + display: 'flex', // Use flexbox layout + marginBottom: '20px', // Space below the input area + gap: '10px', // Space between input and button + }, + // Styles for the input field + input: { + flex: '1', // Take up remaining space + padding: '10px', // Padding inside the input + fontSize: '16px', // Font size + borderRadius: '5px', // Rounded corners + border: '1px solid #ced4da', // Light gray border + }, + // Styles for the set value button + setButton: { + padding: '10px 15px', // Padding inside the button + fontSize: '16px', // Font size + backgroundColor: '#28a745', // Green background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + }, + // Styles for the history container + historyContainer: { + backgroundColor: '#ffffff', // White background + padding: '15px', // Padding inside the box + borderRadius: '8px', // Rounded corners + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', // Subtle shadow + }, + // Styles for the history header (title and clear button) + historyHeader: { + display: 'flex', // Use flexbox layout + justifyContent: 'space-between', // Space title and button apart + alignItems: 'center', // Vertically center items + marginBottom: '10px', // Space below the header + }, + // Styles for the history title + historyTitle: { + margin: '0', // No margin + color: '#343a40', // Dark gray color + }, + // Styles for the clear history button + clearButton: { + padding: '5px 10px', // Padding inside the button + fontSize: '14px', // Font size + backgroundColor: '#dc3545', // Red background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + }, + // Styles for the history list container + historyList: { + maxHeight: '200px', // Maximum height before scrolling + overflowY: 'auto', // Add vertical scroll if needed + }, + // Styles for each history item + historyItem: { + padding: '8px', // Padding inside the item + borderBottom: '1px solid #e9ecef', // Light gray line below each item + color: '#495057', // Medium gray text + }, + // Styles for the empty history message + emptyHistory: { + textAlign: 'center', // Center the text + color: '#6c757d', // Light gray color + fontStyle: 'italic', // Italic text + }, +}; + +// Export the UseStateChallenge component so it can be used in other files +export default UseStateChallenge; \ No newline at end of file diff --git a/Exercise 7/src/components/useContextChallenge/AuthContext.jsx b/Exercise 7/src/components/useContextChallenge/AuthContext.jsx new file mode 100644 index 00000000..51c588e4 --- /dev/null +++ b/Exercise 7/src/components/useContextChallenge/AuthContext.jsx @@ -0,0 +1,200 @@ +// Import React and some useful hooks and functions from the 'react' library +import React, { createContext, useState, useEffect } from 'react'; + +// This is a comment block describing the challenge and requirements for this file +/** + * Challenge: Create an authentication context for user management + * + * Requirements: + * 1. Create a context for authentication state + * 2. Implement login/logout functionality + * 3. Add user profile and preferences + * 4. Create a provider component that will wrap the application + */ + +// Create a new context object for authentication, which will be used to share data across components +export const AuthContext = createContext(); + +// Define the AuthProvider component, which will wrap parts of the app that need authentication info +export const AuthProvider = ({ children }) => { + // Create a state variable to track if the user is authenticated, default is false (not logged in) + const [isAuthenticated, setIsAuthenticated] = useState(false); + // Create a state variable to store user information, default is null (no user) + const [user, setUser] = useState(null); + // Create a state variable to show if the app is loading authentication info, default is true (loading) + const [loading, setLoading] = useState(true); + // Create a state variable to store any error messages, default is null (no error) + const [error, setError] = useState(null); + // Create a state variable for the user's theme preference, default is 'light' + const [theme, setTheme] = useState('light'); + // Create a state variable for notification settings, default is true (notifications on) + const [notifications, setNotifications] = useState(true); + + // useEffect runs code when the component mounts (loads) or updates + useEffect(() => { + // Define an async function to check if the user is already logged in + const checkAuth = async () => { + try { + // Simulate a delay (like an API call) using setTimeout for 1 second + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Try to get stored user data from the browser's localStorage + const storedUser = localStorage.getItem('user'); + // Try to get stored theme preference from localStorage + const storedTheme = localStorage.getItem('theme'); + // Try to get stored notification setting from localStorage + const storedNotifications = localStorage.getItem('notifications'); + + // If there is a stored user, update the user state and set authenticated to true + if (storedUser) { + setUser(JSON.parse(storedUser)); // Parse the user data from JSON + setIsAuthenticated(true); // Set authentication to true + } + + // If there is a stored theme, update the theme state + if (storedTheme) { + setTheme(storedTheme); + } + + // If there is a stored notification setting, update the notifications state + if (storedNotifications !== null) { + setNotifications(JSON.parse(storedNotifications)); // Parse the boolean value + } + } catch (err) { + // If there is an error, set the error state with a message + setError('Failed to authenticate'); + // Also log the error to the console for debugging + console.error('Auth check error:', err); + } finally { + // After everything is done (success or error), set loading to false + setLoading(false); + } + }; + + // Call the checkAuth function when the component mounts + checkAuth(); + }, []); // The empty array means this runs only once when the component mounts + + // Define a function to log in the user, takes email and password as arguments + const login = async (email, password) => { + // Set loading to true while logging in + setLoading(true); + // Clear any previous error messages + setError(null); + + try { + // Simulate a delay (like an API call) using setTimeout for 1 second + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check if the email and password match the demo credentials + if (email === 'user@example.com' && password === 'password') { + // If credentials are correct, create a user object with some info + const userData = { + id: '1', + name: 'Demo User', + email: 'user@example.com', + avatar: 'https://via.placeholder.com/150', + role: 'user', + lastLogin: new Date().toISOString(), // Store the current date/time + }; + + // Update the user state with the new user data + setUser(userData); + // Set authentication to true + setIsAuthenticated(true); + + // Save the user data to localStorage so it persists after refresh + localStorage.setItem('user', JSON.stringify(userData)); + + // Return an object indicating login was successful + return { success: true }; + } else { + // If credentials are wrong, throw an error + throw new Error('Invalid credentials'); + } + } catch (err) { + // If there is an error, set the error state with the error message + setError(err.message || 'Login failed'); + // Return an object indicating login failed, with the error message + return { success: false, error: err.message }; + } finally { + // After everything is done (success or error), set loading to false + setLoading(false); + } + }; + + // Define a function to log out the user + const logout = () => { + // Set authentication to false + setIsAuthenticated(false); + // Clear the user state (no user) + setUser(null); + // Remove the user data from localStorage + localStorage.removeItem('user'); + }; + + // Define a function to update the user's profile with new data + const updateProfile = (updatedData) => { + // Create a new user object by merging the current user and the updated data + const updatedUser = { ...user, ...updatedData }; + // Update the user state with the new user object + setUser(updatedUser); + // Save the updated user data to localStorage + localStorage.setItem('user', JSON.stringify(updatedUser)); + }; + + // Define a function to toggle the theme between 'light' and 'dark' + const toggleTheme = () => { + // If the current theme is 'light', change to 'dark', otherwise change to 'light' + const newTheme = theme === 'light' ? 'dark' : 'light'; + // Update the theme state with the new theme + setTheme(newTheme); + // Save the new theme to localStorage + localStorage.setItem('theme', newTheme); + }; + + + // Define a function to toggle notifications on or off + const toggleNotifications = () => { + // Flip the current notifications value (true becomes false, false becomes true) + const newNotificationSetting = !notifications; + // Update the notifications state with the new value + setNotifications(newNotificationSetting); + // Save the new notifications setting to localStorage + localStorage.setItem('notifications', JSON.stringify(newNotificationSetting)); + }; + + // Create an object with all the values and functions we want to share in the context + const contextValue = { + isAuthenticated, // Whether the user is logged in + user, // The user object (or null if not logged in) + loading, // Whether authentication info is loading + error, // Any error messages + theme, // The user's theme preference + notifications, // Whether notifications are enabled + login, // Function to log in + logout, // Function to log out + updateProfile, // Function to update user profile + toggleTheme, // Function to toggle theme + toggleNotifications, // Function to toggle notifications + }; + + // This is the return statement of our AuthProvider component + // We're returning a special React component called "AuthContext.Provider" +return ( + // AuthContext.Provider is a built-in component from React's Context API + // It allows us to share data (like user info or theme settings) with any nested components + + + {/* This is where we render any child components inside the provider */} + {/* These children will now have access to the contextValue via useContext(AuthContext) */} + {children} + + {/* Closing tag for the AuthContext.Provider */} + +); +} + +// Export the AuthProvider component as the default export from this file +// This allows other files to import and use the AuthProvider +export default AuthProvider; \ No newline at end of file diff --git a/Exercise 7/src/components/useContextChallenge/UseContext.jsx b/Exercise 7/src/components/useContextChallenge/UseContext.jsx new file mode 100644 index 00000000..d615cf2c --- /dev/null +++ b/Exercise 7/src/components/useContextChallenge/UseContext.jsx @@ -0,0 +1,559 @@ +// Import React and two hooks: useContext (to use context) and useState (to manage local state) +import React, { useContext, useState } from 'react'; +// Import AuthContext (the context object) and AuthProvider (the provider component) from AuthContext file +import { AuthContext, AuthProvider } from './AuthContext'; + +/** + * Challenge: Create a component that uses the AuthContext + * + * Requirements: + * 1. Use the AuthContext to access authentication state and functions + * 2. Implement login/logout UI + * 3. Display user profile when logged in + * 4. Allow changing user preferences (theme, notifications) + * 5. Add styling to make it visually appealing + */ + +// Login Form Component +// This component displays the login form and handles login logic +const LoginForm = () => { + // Get login function, error message, and loading state from AuthContext + const { login, error, loading } = useContext(AuthContext); + // Create state for email input + const [email, setEmail] = useState(''); + // Create state for password input + const [password, setPassword] = useState(''); + // Create state to show/hide the login hint + const [showHint, setShowHint] = useState(false); + + // Function to handle form submission + const handleSubmit = async (e) => { + e.preventDefault(); // Prevent default form submission behavior (page reload) + await login(email, password); // Call login function from context with email and password + }; + + // Render the login form UI + return ( +
+ {/* Title for the login form */} +

Login

+ + {/* If there is an error, show it above the form */} + {error &&
{error}
} + + {/* The login form */} +
+ {/* Email input field */} +
+ + setEmail(e.target.value)} // Update email state on change + style={styles.input} // Apply styles + placeholder="Enter your email" // Placeholder text + required // Make this field required + /> +
+ + {/* Password input field */} +
+ + setPassword(e.target.value)} // Update password state on change + style={styles.input} // Apply styles + placeholder="Enter your password" // Placeholder text + required // Make this field required + /> +
+ + {/* Submit button for the form */} + +
+ + {/* Hint section for test credentials */} +
+ {/* Button to show/hide the hint */} + + + {/* If showHint is true, display the hint */} + {showHint && ( +
+

Use these credentials:

+

Email: user@example.com

+

Password: password

+
+ )} +
+
+ ); +}; + +// User Profile Component +// This component displays the user's profile and allows editing preferences +const UserProfile = () => { + // Get user info, logout function, updateProfile function, theme, theme toggle, notifications, and notification toggle from context + const { user, logout, updateProfile, theme, toggleTheme, notifications, toggleNotifications } = useContext(AuthContext); + // State to track if the user is editing their name + const [isEditing, setIsEditing] = useState(false); + // State for the edited name input + const [editedName, setEditedName] = useState(user.name); + + // Function to save the edited name + const handleSaveProfile = () => { + updateProfile({ name: editedName }); // Call updateProfile with new name + setIsEditing(false); // Exit editing mode + }; + + // Render the user profile UI + return ( +
+ {/* Profile header with avatar and user info */} +
+ {/* User avatar */} +
+ {user.name} +
+ + {/* User information section */} +
+ {/* If editing, show input and save/cancel buttons; otherwise, show name and edit button */} + {isEditing ? ( +
+ setEditedName(e.target.value)} // Update editedName on change + style={styles.editNameInput} // Apply styles + /> +
+ {/* Save button */} + + {/* Cancel button */} + +
+
+ ) : ( + // Display user name and edit button +

+ {user.name} + +

+ )} + {/* Display user email */} +

{user.email}

+ {/* Display user role */} +

Role: {user.role}

+ {/* Display last login time, formatted as a readable string */} +

Last login: {new Date(user.lastLogin).toLocaleString()}

+
+
+ + {/* Preferences section for theme and notifications */} +
+

User Preferences

+ + {/* Theme toggle */} +
+ Theme: +
+ +
+
+ + {/* Notifications toggle */} +
+ Notifications: + +
+
+ + {/* Logout button */} + +
+ ); +}; + +// Loading Spinner Component +// This component displays a loading spinner and message +const LoadingSpinner = () => { + // Render spinner and loading text + return ( +
+
+

Loading...

+
+ ); +}; + +// Main Component +// This is the main component that wraps everything in the AuthProvider +const UseContextChallenge = () => { + // Render the AuthProvider and the main UI + return ( + +
+ {/* Title for the challenge */} +

useContext Authentication Challenge

+ {/* Render the AuthConsumer component, which shows login or profile */} + +
+
+ ); +}; + +// Consumer Component +// This component decides whether to show the login form or user profile +const AuthConsumer = () => { + // Get authentication state, loading state, and theme from context + const { isAuthenticated, loading, theme } = useContext(AuthContext); + + // Apply theme styles to the container + const containerStyle = { + ...styles.authContainer, // Spread base styles + backgroundColor: theme === 'light' ? '#ffffff' : '#343a40', // Set background based on theme + color: theme === 'light' ? '#343a40' : '#ffffff', // Set text color based on theme + }; + + // If loading, show the loading spinner + if (loading) { + return ; + } + + // If authenticated, show user profile; otherwise, show login form + return ( +
+ {isAuthenticated ? : } +
+ ); +}; + +// Styles object for inline styling throughout the components +const styles = { + container: { + maxWidth: '800px', // Maximum width of the container + margin: '0 auto', // Center the container horizontally + padding: '20px', // Padding inside the container + backgroundColor: '#f8f9fa', // Light background color + borderRadius: '10px', // Rounded corners + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', // Subtle shadow + fontFamily: 'Arial, sans-serif', // Font family + }, + title: { + textAlign: 'center', // Center the title + color: '#343a40', // Dark text color + marginBottom: '30px', // Space below the title + fontSize: '28px', // Large font size + }, + authContainer: { + padding: '25px', // Padding inside the auth container + borderRadius: '8px', // Rounded corners + boxShadow: '0 2px 10px rgba(0, 0, 0, 0.1)', // Subtle shadow + transition: 'all 0.3s ease', // Smooth transition for style changes + }, + loadingContainer: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack children vertically + alignItems: 'center', // Center children horizontally + justifyContent: 'center', // Center children vertically + padding: '40px', // Padding inside the container + }, + spinner: { + width: '50px', // Spinner width + height: '50px', // Spinner height + border: '5px solid #f3f3f3', // Light gray border + borderTop: '5px solid #007bff', // Blue top border for spinning effect + borderRadius: '50%', // Make it circular + animation: 'spin 1s linear infinite', // Spin animation (requires CSS keyframes) + }, + loadingText: { + marginTop: '15px', // Space above the text + color: '#6c757d', // Gray text color + fontSize: '16px', // Medium font size + }, + loginForm: { + maxWidth: '400px', // Maximum width of the form + margin: '0 auto', // Center the form horizontally + }, + formTitle: { + textAlign: 'center', // Center the form title + marginBottom: '20px', // Space below the title + }, + form: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack form fields vertically + }, + formGroup: { + marginBottom: '15px', // Space below each form group + }, + label: { + marginBottom: '5px', // Space below the label + display: 'block', // Make label a block element + fontSize: '14px', // Small font size + }, + input: { + width: '100%', // Input takes full width + padding: '10px', // Padding inside the input + fontSize: '16px', // Medium font size + borderRadius: '5px', // Rounded corners + border: '1px solid #ced4da', // Light gray border + backgroundColor: 'transparent', // Transparent background + }, + submitButton: { + padding: '12px', // Padding inside the button + backgroundColor: '#007bff', // Blue background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + fontSize: '16px', // Medium font size + cursor: 'pointer', // Pointer cursor on hover + marginTop: '10px', // Space above the button + }, + errorMessage: { + color: '#dc3545', // Red text color + padding: '10px', // Padding inside the error message + borderRadius: '5px', // Rounded corners + backgroundColor: '#f8d7da', // Light red background + marginBottom: '15px', // Space below the error message + textAlign: 'center', // Center the text + }, + hintContainer: { + marginTop: '20px', // Space above the hint container + textAlign: 'center', // Center the content + }, + hintButton: { + backgroundColor: 'transparent', // No background + border: 'none', // No border + color: '#007bff', // Blue text + cursor: 'pointer', // Pointer cursor on hover + fontSize: '14px', // Small font size + textDecoration: 'underline', // Underline the text + }, + hint: { + marginTop: '10px', // Space above the hint + padding: '10px', // Padding inside the hint + backgroundColor: '#e9ecef', // Light gray background + borderRadius: '5px', // Rounded corners + fontSize: '14px', // Small font size + }, + profileContainer: { + display: 'flex', // Use flexbox + flexDirection: 'column', // Stack children vertically + gap: '20px', // Space between children + }, + profileHeader: { + display: 'flex', // Use flexbox + alignItems: 'center', // Center items vertically + gap: '20px', // Space between avatar and info + }, + avatarContainer: { + width: '100px', // Avatar container width + height: '100px', // Avatar container height + borderRadius: '50%', // Make it circular + overflow: 'hidden', // Hide overflow + border: '3px solid #007bff', // Blue border + }, + avatar: { + width: '100%', // Avatar image takes full width + height: '100%', // Avatar image takes full height + objectFit: 'cover', // Cover the container + }, + userInfo: { + flex: '1', // Take up remaining space + }, + userName: { + fontSize: '24px', // Large font size + fontWeight: 'bold', // Bold text + margin: '0 0 5px 0', // Margin below the name + display: 'flex', // Use flexbox + alignItems: 'center', // Center items vertically + gap: '10px', // Space between name and edit button + }, + userEmail: { + fontSize: '16px', // Medium font size + color: '#6c757d', // Gray text color + margin: '0 0 5px 0', // Margin below the email + }, + userRole: { + fontSize: '14px', // Small font size + margin: '0 0 5px 0', // Margin below the role + }, + lastLogin: { + fontSize: '12px', // Small font size + fontStyle: 'italic', // Italic text + margin: '0', // No margin + }, + editButton: { + padding: '3px 8px', // Padding inside the button + fontSize: '12px', // Small font size + backgroundColor: '#6c757d', // Gray background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '3px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + marginLeft: '10px', // Space to the left of the button + }, + editNameContainer: { + marginBottom: '10px', // Space below the edit container + }, + editNameInput: { + width: '100%', // Input takes full width + padding: '8px', // Padding inside the input + fontSize: '16px', // Medium font size + borderRadius: '5px', // Rounded corners + border: '1px solid #ced4da', // Light gray border + marginBottom: '10px', // Space below the input + backgroundColor: 'transparent', // Transparent background + }, + editButtonGroup: { + display: 'flex', // Use flexbox + gap: '10px', // Space between buttons + }, + saveButton: { + padding: '5px 10px', // Padding inside the button + backgroundColor: '#28a745', // Green background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + }, + cancelButton: { + padding: '5px 10px', // Padding inside the button + backgroundColor: '#dc3545', // Red background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + cursor: 'pointer', // Pointer cursor on hover + }, + preferencesSection: { + marginTop: '20px', // Space above the section + padding: '15px', // Padding inside the section + borderRadius: '5px', // Rounded corners + backgroundColor: 'rgba(0, 0, 0, 0.05)', // Light transparent background + }, + preferencesTitle: { + margin: '0 0 15px 0', // Margin below the title + fontSize: '18px', // Medium font size + }, + preferenceItem: { + display: 'flex', // Use flexbox + justifyContent: 'space-between', // Space between label and control + alignItems: 'center', // Center items vertically + marginBottom: '10px', // Space below the item + }, + preferenceLabel: { + fontSize: '16px', // Medium font size + }, + themeToggle: { + display: 'flex', // Use flexbox + alignItems: 'center', // Center items vertically + }, + themeButton: { + padding: '8px 12px', // Padding inside the button + borderRadius: '20px', // Pill-shaped button + border: '1px solid #ced4da', // Light gray border + cursor: 'pointer', // Pointer cursor on hover + transition: 'all 0.3s ease', // Smooth transition for style changes + }, + switch: { + position: 'relative', // Position relative for slider + display: 'inline-block', // Inline-block element + width: '60px', // Width of the switch + height: '34px', // Height of the switch + }, + slider: { + position: 'absolute', // Position absolute for slider + cursor: 'pointer', // Pointer cursor on hover + top: '0', // Top position + left: '0', // Left position + right: '0', // Right position + bottom: '0', // Bottom position + backgroundColor: '#ccc', // Gray background + borderRadius: '34px', // Rounded slider + transition: '0.4s', // Smooth transition + '&:before': { + position: 'absolute', // Position absolute for the knob + content: '""', // Empty content for the knob + height: '26px', // Height of the knob + width: '26px', // Width of the knob + left: '4px', // Left position + bottom: '4px', // Bottom position + backgroundColor: 'white', // White knob + borderRadius: '50%', // Circular knob + transition: '0.4s', // Smooth transition + }, + }, + logoutButton: { + padding: '12px', // Padding inside the button + backgroundColor: '#dc3545', // Red background + color: '#ffffff', // White text + border: 'none', // No border + borderRadius: '5px', // Rounded corners + fontSize: '16px', // Medium font size + cursor: 'pointer', // Pointer cursor on hover + marginTop: '10px', // Space above the button + width: '100%', // Button takes full width + }, +}; + +// Export the main component as the default export +export default UseContextChallenge; \ No newline at end of file diff --git a/Exercise 7/src/index.css b/Exercise 7/src/index.css new file mode 100644 index 00000000..151514fc --- /dev/null +++ b/Exercise 7/src/index.css @@ -0,0 +1,53 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} diff --git a/Exercise 7/src/index.jsx b/Exercise 7/src/index.jsx new file mode 100644 index 00000000..8ed7c441 --- /dev/null +++ b/Exercise 7/src/index.jsx @@ -0,0 +1,15 @@ +// index.jsx is what allows us to run the App.jsx file + +// import React and ReactDOM from the react and react-dom packages +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; + +// import the App component from the App.jsx file +// the 'body' equivalent in HTML +// the 'root' element is where the App component will be rendered +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/Exercise 7/vite.config.js b/Exercise 7/vite.config.js new file mode 100644 index 00000000..a0f4db5a --- /dev/null +++ b/Exercise 7/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite" +import react from "@vitejs/plugin-react" + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3000 + }, + plugins: [react()] +}) diff --git a/PDFs/Bootstrap_Cheat-Sheet.pdf b/PDFs/Bootstrap_Cheat-Sheet.pdf new file mode 100644 index 00000000..dc2fe553 Binary files /dev/null and b/PDFs/Bootstrap_Cheat-Sheet.pdf differ diff --git a/PDFs/CSS_Attributes_Cheat_Sheet.pdf b/PDFs/CSS_Attributes_Cheat_Sheet.pdf new file mode 100644 index 00000000..d264504f Binary files /dev/null and b/PDFs/CSS_Attributes_Cheat_Sheet.pdf differ diff --git a/PDFs/CSS_Selectors_Cheat-Sheet.pdf b/PDFs/CSS_Selectors_Cheat-Sheet.pdf new file mode 100644 index 00000000..0a693e3d Binary files /dev/null and b/PDFs/CSS_Selectors_Cheat-Sheet.pdf differ diff --git a/PDFs/JS_React_Syntax_Cheat_Sheet.pdf b/PDFs/JS_React_Syntax_Cheat_Sheet.pdf new file mode 100644 index 00000000..eedfae06 Binary files /dev/null and b/PDFs/JS_React_Syntax_Cheat_Sheet.pdf differ diff --git a/PDFs/React_Cheat_Sheet.pdf b/PDFs/React_Cheat_Sheet.pdf new file mode 100644 index 00000000..eb1b6baf Binary files /dev/null and b/PDFs/React_Cheat_Sheet.pdf differ