mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-03-11 17:49:25 -05:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -125,13 +125,6 @@
|
||||
"profile": "https://github.com/jaysonzlin",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "andreamurillomtz",
|
||||
"name": "Andrea",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/andreamurillomtz",
|
||||
"profile": "https://github.com/andreamurillomtz",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "sophiacho1",
|
||||
"name": "Sophia Cho",
|
||||
@@ -139,6 +132,13 @@
|
||||
"profile": "https://github.com/sophiacho1",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "andreamurillomtz",
|
||||
"name": "Andrea",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/andreamurillomtz",
|
||||
"profile": "https://github.com/andreamurillomtz",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "alxrod",
|
||||
"name": "Alex Rodriguez",
|
||||
@@ -181,6 +181,13 @@
|
||||
"profile": "https://github.com/colbybanbury",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "GabrielAmazonas",
|
||||
"name": "Gabriel Amazonas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/GabrielAmazonas",
|
||||
"profile": "https://github.com/GabrielAmazonas",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "mmaz",
|
||||
"name": "Mark Mazumder",
|
||||
@@ -189,10 +196,10 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "GabrielAmazonas",
|
||||
"name": "Gabriel Amazonas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/GabrielAmazonas",
|
||||
"profile": "https://github.com/GabrielAmazonas",
|
||||
"login": "ma3mool",
|
||||
"name": "Abdulrahman Mahmoud",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/ma3mool",
|
||||
"profile": "https://github.com/ma3mool",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -209,13 +216,6 @@
|
||||
"profile": "https://github.com/srivatsankrishnan",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "ma3mool",
|
||||
"name": "Abdulrahman Mahmoud",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/ma3mool",
|
||||
"profile": "https://github.com/ma3mool",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "aptl26",
|
||||
"name": "Aghyad Deeb",
|
||||
@@ -223,13 +223,6 @@
|
||||
"profile": "https://github.com/aptl26",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "arnaumarin",
|
||||
"name": "marin-llobet",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/arnaumarin",
|
||||
"profile": "https://github.com/arnaumarin",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "James-QiuHaoran",
|
||||
"name": "Haoran Qiu",
|
||||
@@ -238,17 +231,10 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "AditiR-42",
|
||||
"name": "Aditi Raju",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/AditiR-42",
|
||||
"profile": "https://github.com/AditiR-42",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "MichaelSchnebly",
|
||||
"name": "Michael Schnebly",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/MichaelSchnebly",
|
||||
"profile": "https://github.com/MichaelSchnebly",
|
||||
"login": "arnaumarin",
|
||||
"name": "marin-llobet",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/arnaumarin",
|
||||
"profile": "https://github.com/arnaumarin",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -259,10 +245,10 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "ELSuitorHarvard",
|
||||
"name": "ELSuitorHarvard",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/ELSuitorHarvard",
|
||||
"profile": "https://github.com/ELSuitorHarvard",
|
||||
"login": "MichaelSchnebly",
|
||||
"name": "Michael Schnebly",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/MichaelSchnebly",
|
||||
"profile": "https://github.com/MichaelSchnebly",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -273,17 +259,10 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "kaiM0ves",
|
||||
"name": "kaiM0ves",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/kaiM0ves",
|
||||
"profile": "https://github.com/kaiM0ves",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jared-ni",
|
||||
"name": "Jared Ni",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jared-ni",
|
||||
"profile": "https://github.com/jared-ni",
|
||||
"login": "ELSuitorHarvard",
|
||||
"name": "ELSuitorHarvard",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/ELSuitorHarvard",
|
||||
"profile": "https://github.com/ELSuitorHarvard",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -293,6 +272,27 @@
|
||||
"profile": "https://github.com/oishib",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jared-ni",
|
||||
"name": "Jared Ni",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jared-ni",
|
||||
"profile": "https://github.com/jared-ni",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "kaiM0ves",
|
||||
"name": "kaiM0ves",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/kaiM0ves",
|
||||
"profile": "https://github.com/kaiM0ves",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "AditiR-42",
|
||||
"name": "Aditi Raju",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/AditiR-42",
|
||||
"profile": "https://github.com/AditiR-42",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "eimlav",
|
||||
"name": "Eimhin Laverty",
|
||||
@@ -300,20 +300,6 @@
|
||||
"profile": "https://github.com/eimlav",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "leo47007",
|
||||
"name": "Yu-Shun Hsiao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/leo47007",
|
||||
"profile": "https://github.com/leo47007",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jaywonchung",
|
||||
"name": "Jae-Won Chung",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jaywonchung",
|
||||
"profile": "https://github.com/jaywonchung",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "BaeHenryS",
|
||||
"name": "Henry Bae",
|
||||
@@ -321,6 +307,20 @@
|
||||
"profile": "https://github.com/BaeHenryS",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jaywonchung",
|
||||
"name": "Jae-Won Chung",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jaywonchung",
|
||||
"profile": "https://github.com/jaywonchung",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "leo47007",
|
||||
"name": "Yu-Shun Hsiao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/leo47007",
|
||||
"profile": "https://github.com/leo47007",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "AndreaMattiaGaravagno",
|
||||
"name": "AndreaMattiaGaravagno",
|
||||
@@ -329,10 +329,31 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "pongtr",
|
||||
"name": "Pong Trairatvorakul",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/pongtr",
|
||||
"profile": "https://github.com/pongtr",
|
||||
"login": "arbass22",
|
||||
"name": "Andrew Bass",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/arbass22",
|
||||
"profile": "https://github.com/arbass22",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "aryatschand",
|
||||
"name": "Arya Tschand",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/aryatschand",
|
||||
"profile": "https://github.com/aryatschand",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "ShvetankPrakash",
|
||||
"name": "Shvetank Prakash",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/ShvetankPrakash",
|
||||
"profile": "https://github.com/ShvetankPrakash",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jianqingdu",
|
||||
"name": "jianqingdu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jianqingdu",
|
||||
"profile": "https://github.com/jianqingdu",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -349,41 +370,6 @@
|
||||
"profile": "https://github.com/harvard-edge/cs249r_book/graphs/contributors",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "marcozennaro",
|
||||
"name": "Marco Zennaro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/marcozennaro",
|
||||
"profile": "https://github.com/marcozennaro",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jianqingdu",
|
||||
"name": "jianqingdu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jianqingdu",
|
||||
"profile": "https://github.com/jianqingdu",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "ShvetankPrakash",
|
||||
"name": "Shvetank Prakash",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/ShvetankPrakash",
|
||||
"profile": "https://github.com/ShvetankPrakash",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "aryatschand",
|
||||
"name": "Arya Tschand",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/aryatschand",
|
||||
"profile": "https://github.com/aryatschand",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "arbass22",
|
||||
"name": "Andrew Bass",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/arbass22",
|
||||
"profile": "https://github.com/arbass22",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jzhou1318",
|
||||
"name": "Jennifer Zhou",
|
||||
@@ -399,24 +385,24 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Fatima Shah",
|
||||
"name": "Fatima Shah",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/468ef35acc69f3266efd700992daa369?d=identicon&s=100",
|
||||
"profile": "https://github.com/harvard-edge/cs249r_book/graphs/contributors",
|
||||
"login": "marcozennaro",
|
||||
"name": "Marco Zennaro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/marcozennaro",
|
||||
"profile": "https://github.com/marcozennaro",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "taunoe",
|
||||
"name": "Tauno Erik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/taunoe",
|
||||
"profile": "https://github.com/taunoe",
|
||||
"login": "pongtr",
|
||||
"name": "Pong Trairatvorakul",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/pongtr",
|
||||
"profile": "https://github.com/pongtr",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "serco425",
|
||||
"name": "Sercan Ayg\u00fcn",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/serco425",
|
||||
"profile": "https://github.com/serco425",
|
||||
"login": "alex-oesterling",
|
||||
"name": "Alex Oesterling",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/alex-oesterling",
|
||||
"profile": "https://github.com/alex-oesterling",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -426,6 +412,13 @@
|
||||
"profile": "https://github.com/harvard-edge/cs249r_book/graphs/contributors",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Fatima Shah",
|
||||
"name": "Fatima Shah",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/468ef35acc69f3266efd700992daa369?d=identicon&s=100",
|
||||
"profile": "https://github.com/harvard-edge/cs249r_book/graphs/contributors",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "vitasam",
|
||||
"name": "The Random DIY",
|
||||
@@ -433,20 +426,6 @@
|
||||
"profile": "https://github.com/vitasam",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "TheHiddenLayer",
|
||||
"name": "TheHiddenLayer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/TheHiddenLayer",
|
||||
"profile": "https://github.com/TheHiddenLayer",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "FinAminToastCrunch",
|
||||
"name": "Fin Amin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/FinAminToastCrunch",
|
||||
"profile": "https://github.com/FinAminToastCrunch",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "BunningsWarehouseOfficial",
|
||||
"name": "Kristian Rado\u0161",
|
||||
@@ -454,34 +433,6 @@
|
||||
"profile": "https://github.com/BunningsWarehouseOfficial",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "BrunoScaglione",
|
||||
"name": "Bruno Scaglione",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/BrunoScaglione",
|
||||
"profile": "https://github.com/BrunoScaglione",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Allen-Kuang",
|
||||
"name": "Allen-Kuang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/Allen-Kuang",
|
||||
"profile": "https://github.com/Allen-Kuang",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "alex-oesterling",
|
||||
"name": "Alex Oesterling",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/alex-oesterling",
|
||||
"profile": "https://github.com/alex-oesterling",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "gnodipac886",
|
||||
"name": "gnodipac886",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/gnodipac886",
|
||||
"profile": "https://github.com/gnodipac886",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Gjain234",
|
||||
"name": "Gauri Jain",
|
||||
@@ -490,17 +441,10 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "aethernavshulkraven-allain",
|
||||
"name": "\u0905\u0930\u0928\u0935 \u0936\u0941\u0915\u094d\u0932\u093e | Arnav Shukla",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/aethernavshulkraven-allain",
|
||||
"profile": "https://github.com/aethernavshulkraven-allain",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "KarthikDani",
|
||||
"name": "Karthik Dani",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/KarthikDani",
|
||||
"profile": "https://github.com/KarthikDani",
|
||||
"login": "taunoe",
|
||||
"name": "Tauno Erik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/taunoe",
|
||||
"profile": "https://github.com/taunoe",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -510,6 +454,69 @@
|
||||
"profile": "https://github.com/RinZ27",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "TheHiddenLayer",
|
||||
"name": "TheHiddenLayer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/TheHiddenLayer",
|
||||
"profile": "https://github.com/TheHiddenLayer",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Allen-Kuang",
|
||||
"name": "Allen-Kuang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/Allen-Kuang",
|
||||
"profile": "https://github.com/Allen-Kuang",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "FinAminToastCrunch",
|
||||
"name": "Fin Amin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/FinAminToastCrunch",
|
||||
"profile": "https://github.com/FinAminToastCrunch",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "BrunoScaglione",
|
||||
"name": "Bruno Scaglione",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/BrunoScaglione",
|
||||
"profile": "https://github.com/BrunoScaglione",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "serco425",
|
||||
"name": "Sercan Ayg\u00fcn",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/serco425",
|
||||
"profile": "https://github.com/serco425",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "gnodipac886",
|
||||
"name": "gnodipac886",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/gnodipac886",
|
||||
"profile": "https://github.com/gnodipac886",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "YangZhou1997",
|
||||
"name": "Yang Zhou",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/YangZhou1997",
|
||||
"profile": "https://github.com/YangZhou1997",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "aethernavshulkraven-allain",
|
||||
"name": "\u0905\u0930\u0928\u0935 \u0936\u0941\u0915\u094d\u0932\u093e | Arnav Shukla",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/aethernavshulkraven-allain",
|
||||
"profile": "https://github.com/aethernavshulkraven-allain",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "arighosh05",
|
||||
"name": "Aritra Ghosh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/arighosh05",
|
||||
"profile": "https://github.com/arighosh05",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "XaicuL",
|
||||
"name": "JEON HYUNJUN(Luciano)",
|
||||
@@ -517,6 +524,20 @@
|
||||
"profile": "https://github.com/XaicuL",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "KarthikDani",
|
||||
"name": "Karthik Dani",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/KarthikDani",
|
||||
"profile": "https://github.com/KarthikDani",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Pratham-ja",
|
||||
"name": "Pratham Chaudhary",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/Pratham-ja",
|
||||
"profile": "https://github.com/Pratham-ja",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Jahnic-kb",
|
||||
"name": "Jahnic Beck",
|
||||
@@ -524,13 +545,6 @@
|
||||
"profile": "https://github.com/Jahnic-kb",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "AbenezerKb",
|
||||
"name": "Abenezer Angamo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/AbenezerKb",
|
||||
"profile": "https://github.com/AbenezerKb",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "BravoBaldo",
|
||||
"name": "Baldassarre Cesarano",
|
||||
@@ -538,6 +552,20 @@
|
||||
"profile": "https://github.com/BravoBaldo",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "AbenezerKb",
|
||||
"name": "Abenezer Angamo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/AbenezerKb",
|
||||
"profile": "https://github.com/AbenezerKb",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "abigailswallow",
|
||||
"name": "abigailswallow",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/abigailswallow",
|
||||
"profile": "https://github.com/abigailswallow",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "adil-mubashir-ch",
|
||||
"name": "Adil Mubashir Chaudhry",
|
||||
@@ -545,13 +573,6 @@
|
||||
"profile": "https://github.com/adil-mubashir-ch",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "YangZhou1997",
|
||||
"name": "Yang Zhou",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/YangZhou1997",
|
||||
"profile": "https://github.com/YangZhou1997",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "cursoragent",
|
||||
"name": "Cursor Agent",
|
||||
@@ -566,13 +587,6 @@
|
||||
"profile": "https://github.com/bilgeacun",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "arighosh05",
|
||||
"name": "Aritra Ghosh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/arighosh05",
|
||||
"profile": "https://github.com/arighosh05",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "atcheng2",
|
||||
"name": "Andy Cheng",
|
||||
@@ -581,17 +595,10 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "abigailswallow",
|
||||
"name": "abigailswallow",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/abigailswallow",
|
||||
"profile": "https://github.com/abigailswallow",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "emmanuel2406",
|
||||
"name": "Emmanuel Rassou",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/emmanuel2406",
|
||||
"profile": "https://github.com/emmanuel2406",
|
||||
"login": "salmanmkc",
|
||||
"name": "Salman Chishti",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/salmanmkc",
|
||||
"profile": "https://github.com/salmanmkc",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -601,6 +608,20 @@
|
||||
"profile": "https://github.com/happyappledog",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "emmanuel2406",
|
||||
"name": "Emmanuel Rassou",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/emmanuel2406",
|
||||
"profile": "https://github.com/emmanuel2406",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jasonlyik",
|
||||
"name": "Jason Yik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jasonlyik",
|
||||
"profile": "https://github.com/jasonlyik",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jessicaquaye",
|
||||
"name": "Jessica Quaye",
|
||||
@@ -608,6 +629,13 @@
|
||||
"profile": "https://github.com/jessicaquaye",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "skmur",
|
||||
"name": "Sonia Murthy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/skmur",
|
||||
"profile": "https://github.com/skmur",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "sjohri20",
|
||||
"name": "Shreya Johri",
|
||||
@@ -622,20 +650,6 @@
|
||||
"profile": "https://github.com/pipme",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "jasonlyik",
|
||||
"name": "Jason Yik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/jasonlyik",
|
||||
"profile": "https://github.com/jasonlyik",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "snuggs",
|
||||
"name": "Snuggs",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/snuggs",
|
||||
"profile": "https://github.com/snuggs",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "swilcock0",
|
||||
"name": "Sam Wilcock",
|
||||
@@ -644,10 +658,10 @@
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "skmur",
|
||||
"name": "Sonia Murthy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/skmur",
|
||||
"profile": "https://github.com/skmur",
|
||||
"login": "snuggs",
|
||||
"name": "Snuggs",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/snuggs",
|
||||
"profile": "https://github.com/snuggs",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
@@ -713,6 +727,13 @@
|
||||
"profile": "https://github.com/harvard-edge/cs249r_book/graphs/contributors",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Pratham-ja",
|
||||
"name": "Pratham-ja",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/438310e71ca943b41e92065c2dbbddb6?d=identicon&s=100",
|
||||
"profile": "https://github.com/harvard-edge/cs249r_book/graphs/contributors",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "Edward Jin",
|
||||
"name": "Edward Jin",
|
||||
|
||||
@@ -520,6 +520,7 @@ Thanks goes to these wonderful people who have contributed to making this resour
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://github.com/harishb00a"><img src="https://avatars.githubusercontent.com/harishb00a?v=4?s=50" width="50px;" alt="harishb00a"/><br /><sub><b>harishb00a</b></sub></a><br />✍️</td>
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://github.com/sotoblanco"><img src="https://avatars.githubusercontent.com/u/46135649?v=4?v=4?s=50" width="50px;" alt="Pastor Soto"/><br /><sub><b>Pastor Soto</b></sub></a><br />✍️</td>
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://github.com/salmanmkc"><img src="https://avatars.githubusercontent.com/u/32169182?v=4?v=4?s=50" width="50px;" alt="Salman Chishti"/><br /><sub><b>Salman Chishti</b></sub></a><br />🧑💻</td>
|
||||
<td align="center" valign="top" width="11.11%"><a href="https://github.com/adityamulik"><img src="https://avatars.githubusercontent.com/u/10626835?v=4?v=4?s=50" width="50px;" alt="Aditya Mulik"/><br /><sub><b>Aditya Mulik</b></sub></a><br />✍️</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -234,6 +234,15 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "adityamulik",
|
||||
"name": "Aditya Mulik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10626835?v=4",
|
||||
"profile": "https://github.com/adityamulik",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -295,6 +295,7 @@ Thanks to these wonderful people who helped improve TinyTorch!
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/salmanmkc"><img src="https://avatars.githubusercontent.com/u/32169182?v=4?v=4?s=80" width="80px;" alt="Salman Chishti"/><br /><sub><b>Salman Chishti</b></sub></a><br />🧑💻</td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/adityamulik"><img src="https://avatars.githubusercontent.com/u/10626835?v=4?v=4?s=80" width="80px;" alt="Aditya Mulik"/><br /><sub><b>Aditya Mulik</b></sub></a><br />✍️</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
8
tinytorch/site/extra/community/.gitignore
vendored
Normal file
8
tinytorch/site/extra/community/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Secrets
|
||||
tests/e2e/credentials.json
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
playwright/.cache/
|
||||
@@ -1,8 +1,8 @@
|
||||
import { injectStyles } from './modules/styles.js';
|
||||
import { renderLayout, updateNavState } from './modules/ui.js?v=3';
|
||||
import { getSession } from './modules/state.js?v=2';
|
||||
import { openModal, closeModal, handleToggle, handleAuth, handleLogout, setMode, verifySession, signInWithSocial, supabase } from './modules/auth.js?v=3';
|
||||
import { openProfileModal, closeProfileModal, handleProfileUpdate, geocodeAndSetCoordinates, checkAndAutoUpdateLocation } from './modules/profile.js';
|
||||
import { renderLayout, updateNavState } from './modules/ui.js';
|
||||
import { getSession } from './modules/state.js';
|
||||
import { openModal, closeModal, handleToggle, handleAuth, handleLogout, setMode, verifySession, signInWithSocial, supabase } from './modules/auth.js';
|
||||
import { openProfileModal, closeProfileModal, handleProfileUpdate, geocodeAndSetCoordinates, checkAndAutoUpdateLocation, setupProfileDeleteEvents } from './modules/profile.js';
|
||||
import { setupCameraEvents } from './modules/camera.js';
|
||||
import { getBasePath } from './modules/config.js';
|
||||
|
||||
@@ -32,24 +32,8 @@ import { getBasePath } from './modules/config.js';
|
||||
});
|
||||
}
|
||||
|
||||
// 2.6 Check for Supabase Session & Verify
|
||||
const checkProfile = async (session) => {
|
||||
if (!session || window.location.pathname.includes('profile_setup')) return;
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('display_name, institution, location')
|
||||
.eq('id', session.user.id)
|
||||
.single();
|
||||
|
||||
const hasName = profile && profile.display_name;
|
||||
const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution);
|
||||
const hasLoc = profile && profile.location;
|
||||
|
||||
if (!hasName || !hasInst || !hasLoc) {
|
||||
window.location.href = getBasePath() + '/profile_setup.html';
|
||||
}
|
||||
};
|
||||
// Initialize profile events
|
||||
setupProfileDeleteEvents();
|
||||
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
if (session) {
|
||||
@@ -57,13 +41,18 @@ import { getBasePath } from './modules/config.js';
|
||||
if (session.refresh_token) localStorage.setItem("tinytorch_refresh_token", session.refresh_token);
|
||||
if (session.user) localStorage.setItem("tinytorch_user", JSON.stringify(session.user));
|
||||
|
||||
// Clean URL hash if present (Supabase puts tokens there)
|
||||
if (window.location.hash && window.location.hash.includes('access_token')) {
|
||||
window.history.replaceState({}, document.title, window.location.pathname + window.location.search);
|
||||
// Clean URL of tokens/code (Supabase puts tokens in hash or code in query)
|
||||
if ((window.location.hash && window.location.hash.includes('access_token')) ||
|
||||
(window.location.search && window.location.search.includes('code='))) {
|
||||
// Remove sensitive parameters while preserving non-sensitive ones
|
||||
const url = new URL(window.location);
|
||||
url.hash = '';
|
||||
url.searchParams.delete('code');
|
||||
url.searchParams.delete('type');
|
||||
window.history.replaceState({}, document.title, url.toString());
|
||||
}
|
||||
|
||||
updateNavState();
|
||||
checkProfile(session);
|
||||
}
|
||||
// 3. Verify Session (Async)
|
||||
verifySession();
|
||||
@@ -79,7 +68,6 @@ import { getBasePath } from './modules/config.js';
|
||||
if (session.refresh_token) localStorage.setItem("tinytorch_refresh_token", session.refresh_token);
|
||||
if (session.user) localStorage.setItem("tinytorch_user", JSON.stringify(session.user));
|
||||
updateNavState();
|
||||
checkProfile(session);
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
localStorage.removeItem("tinytorch_token");
|
||||
updateNavState();
|
||||
@@ -168,10 +156,13 @@ import { getBasePath } from './modules/config.js';
|
||||
const action = params.get('action');
|
||||
|
||||
if (action === 'login') {
|
||||
localStorage.removeItem("tinytorch_token");
|
||||
localStorage.removeItem("tinytorch_refresh_token");
|
||||
localStorage.removeItem("tinytorch_user");
|
||||
updateNavState();
|
||||
const { isLoggedIn } = getSession();
|
||||
if (!isLoggedIn) {
|
||||
localStorage.removeItem("tinytorch_token");
|
||||
localStorage.removeItem("tinytorch_refresh_token");
|
||||
localStorage.removeItem("tinytorch_user");
|
||||
updateNavState();
|
||||
}
|
||||
openModal('login');
|
||||
} else if (action === 'profile') {
|
||||
const { isLoggedIn } = getSession();
|
||||
|
||||
@@ -289,15 +289,18 @@
|
||||
|
||||
<div class="legend-row">
|
||||
<span class="legend-dot"></span>
|
||||
<span id="member-count">0 Members</span>
|
||||
<span id="member-count">0 Members on Map</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<span class="legend-dot" style="background-color: #2ecc71; border-color: #27ae60;"></span>
|
||||
<span id="no-location-count">0 without Location (Lost at Sea)</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<span class="legend-dot-institution"></span>
|
||||
<span id="institution-count">0 Institutions</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<span class="legend-dot" style="background-color: #2ecc71; border-color: #27ae60;"></span>
|
||||
<span>Members with Unknown Location</span>
|
||||
<div class="legend-row" style="margin-top: 5px; border-top: 1px dashed #eee; padding-top: 8px; font-style: italic; color: #888;">
|
||||
Note: Members without a location set in their profile are placed at the Sea Station.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -307,7 +310,6 @@
|
||||
<svg id="globe-svg"></svg>
|
||||
|
||||
<script type="module">
|
||||
import { initCloud, updateCloudUsers, animateCloud } from './modules/cloud.js';
|
||||
import './app.js'; // Keep existing app logic
|
||||
|
||||
const width = window.innerWidth;
|
||||
@@ -317,7 +319,8 @@
|
||||
const config = {
|
||||
speed: 0.3,
|
||||
verticalTilt: -20,
|
||||
scale: Math.min(width, height) / 2.5
|
||||
scale: Math.min(width, height) / 2.5,
|
||||
lostStationCoords: [-135, -35] // Point in the Pacific
|
||||
};
|
||||
|
||||
// Major cities for rough location mapping
|
||||
@@ -345,7 +348,9 @@
|
||||
{lat: 22.5726, lng: 88.3639, name: "Kolkata"}
|
||||
];
|
||||
|
||||
let totalMemberCount = 0;
|
||||
let locations = [];
|
||||
let lostUsers = [];
|
||||
let institutions = [];
|
||||
const API_URL = "https://zrvmjrxhokwwmjacyhpq.supabase.co/functions/v1/search-profiles";
|
||||
|
||||
@@ -372,11 +377,9 @@
|
||||
const graticuleGroup = svg.append('g');
|
||||
const mapGroup = svg.append('g');
|
||||
const institutionGroup = svg.append('g');
|
||||
const lostStationGroup = svg.append('g');
|
||||
const markerGroup = svg.append('g');
|
||||
|
||||
// Initialize Cloud Module
|
||||
initCloud(svg, width, height, tooltip);
|
||||
|
||||
// 1. Globe Background
|
||||
globeGroup.append("path")
|
||||
.datum({type: "Sphere"})
|
||||
@@ -393,9 +396,9 @@
|
||||
// 3. Load World Data
|
||||
d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json')
|
||||
.then(worldData => {
|
||||
mapGroup.selectAll(".country")
|
||||
.data(topojson.feature(worldData, worldData.objects.countries).features)
|
||||
.enter().append("path")
|
||||
// Optimized: Render countries as a single path
|
||||
mapGroup.append("path")
|
||||
.datum(topojson.feature(worldData, worldData.objects.countries))
|
||||
.attr("class", "country")
|
||||
.attr("d", path);
|
||||
|
||||
@@ -403,119 +406,77 @@
|
||||
fetchProfiles();
|
||||
});
|
||||
|
||||
// ... (Geocoding helpers omitted for brevity, logic preserved) ...
|
||||
// Re-implementing simplified helpers for completeness
|
||||
const geocodeCache = {};
|
||||
let lastGeocodingTime = 0;
|
||||
const GEOCODING_DELAY = 1100;
|
||||
|
||||
function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
|
||||
|
||||
async function getCoords(locationStr) {
|
||||
if (!locationStr || locationStr === "unknown") return getRoughCoords("unknown");
|
||||
if (geocodeCache[locationStr]) return geocodeCache[locationStr];
|
||||
|
||||
try {
|
||||
const now = Date.now();
|
||||
if (now - lastGeocodingTime < GEOCODING_DELAY) await sleep(GEOCODING_DELAY - (now - lastGeocodingTime));
|
||||
lastGeocodingTime = Date.now();
|
||||
|
||||
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(locationStr)}&format=json&limit=1`;
|
||||
const response = await fetch(url, { headers: { 'User-Agent': 'TinyTorch Globe' } });
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const coords = { latitude: parseFloat(data[0].lat), longitude: parseFloat(data[0].lon) };
|
||||
geocodeCache[locationStr] = coords;
|
||||
return coords;
|
||||
}
|
||||
} catch (e) {}
|
||||
return getRoughCoords(locationStr);
|
||||
}
|
||||
|
||||
function getRoughCoords(str) {
|
||||
if (!str) str = "unknown";
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); }
|
||||
const index = Math.abs(hash) % CITIES.length;
|
||||
const city = CITIES[index];
|
||||
return {
|
||||
latitude: city.lat + (Math.random() - 0.5) * 5,
|
||||
longitude: city.lng + (Math.random() - 0.5) * 5
|
||||
};
|
||||
}
|
||||
|
||||
// API Fetch Logic
|
||||
async function fetchProfiles(query = "") {
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ query: query, page: 0, limit: 20 })
|
||||
body: JSON.stringify({ query: query, page: 0, limit: 1000 })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("API Error");
|
||||
const data = await response.json();
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
if (query) locations = [];
|
||||
totalMemberCount = data.length;
|
||||
let mapUsers = [];
|
||||
let tempLostUsers = [];
|
||||
|
||||
locations = data.map(p => {
|
||||
const roughCoords = (p.latitude && p.longitude)
|
||||
? { latitude: p.latitude, longitude: p.longitude }
|
||||
: getRoughCoords(p.location || p.institution || p.username);
|
||||
data.forEach(p => {
|
||||
const lat = parseFloat(p.latitude);
|
||||
const lng = parseFloat(p.longitude);
|
||||
const isLostLabel = p.location === "Lost at Sea 🌊";
|
||||
const hasNoLocation = !p.location || p.location === "unknown";
|
||||
const hasNoCoords = isNaN(lat) || isNaN(lng) || (Math.abs(lat) < 0.001 && Math.abs(lng) < 0.001);
|
||||
|
||||
const displayName = (p.display_name && p.display_name.trim()) ? p.display_name.trim() : (p.username ? p.username.split('@')[0].trim() : 'Anonymous');
|
||||
|
||||
return {
|
||||
latitude: roughCoords.latitude,
|
||||
longitude: roughCoords.longitude,
|
||||
|
||||
const userObj = {
|
||||
user: p.username,
|
||||
displayName: displayName,
|
||||
completed: "Member",
|
||||
institution: Array.isArray(p.institution) ? p.institution.join(", ") : (p.institution || "Independent"),
|
||||
location: p.location || null
|
||||
};
|
||||
|
||||
if (isLostLabel || (hasNoLocation && hasNoCoords)) {
|
||||
tempLostUsers.push(userObj);
|
||||
} else if (!hasNoCoords) {
|
||||
mapUsers.push({
|
||||
...userObj,
|
||||
latitude: lat + (Math.random() - 0.5) * 0.2,
|
||||
longitude: lng + (Math.random() - 0.5) * 0.2
|
||||
});
|
||||
} else {
|
||||
tempLostUsers.push(userObj);
|
||||
}
|
||||
});
|
||||
|
||||
locations = mapUsers;
|
||||
lostUsers = tempLostUsers;
|
||||
|
||||
renderMarkers();
|
||||
renderInstitutions();
|
||||
|
||||
data.forEach(async (p, index) => {
|
||||
const locationString = p.location || p.institution;
|
||||
if (locationString && !(p.latitude && p.longitude)) {
|
||||
const coords = await getCoords(locationString);
|
||||
if (locations[index]) {
|
||||
locations[index].latitude = coords.latitude;
|
||||
locations[index].longitude = coords.longitude;
|
||||
updateMarkerPositions();
|
||||
updateInstitutionPositions();
|
||||
}
|
||||
}
|
||||
});
|
||||
renderLostStation();
|
||||
} else {
|
||||
if (query) locations = [];
|
||||
if (query) { locations = []; lostUsers = []; totalMemberCount = 0; }
|
||||
}
|
||||
} catch (e) {
|
||||
locations = [];
|
||||
lostUsers = [];
|
||||
totalMemberCount = 0;
|
||||
}
|
||||
renderMarkers();
|
||||
renderInstitutions();
|
||||
renderLostStation();
|
||||
if (query) updateSearchDropdown(query);
|
||||
}
|
||||
|
||||
// Render Markers
|
||||
function renderMarkers() {
|
||||
// Split Users
|
||||
const globeUsers = locations.filter(d => d.location !== "Lost at Sea 🌊");
|
||||
const cloudUsers = locations.filter(d => d.location === "Lost at Sea 🌊");
|
||||
|
||||
// Update Cloud
|
||||
updateCloudUsers(cloudUsers);
|
||||
|
||||
// Render Globe Markers
|
||||
const markers = markerGroup.selectAll('circle.marker')
|
||||
.data(globeUsers, d => d.user);
|
||||
.data(locations, d => d.user);
|
||||
|
||||
markers.exit().remove();
|
||||
|
||||
@@ -533,12 +494,41 @@
|
||||
d3.select(this).attr("r", 4);
|
||||
tooltip.style("opacity", 0);
|
||||
})
|
||||
.style('opacity', 0)
|
||||
.transition().duration(300).style('opacity', 1);
|
||||
|
||||
document.getElementById('member-count').textContent = `${locations.length + 10} Members`;
|
||||
document.getElementById('member-count').textContent = `${locations.length} Members on Map`;
|
||||
document.getElementById('no-location-count').textContent = `${lostUsers.length} without Location (Lost at Sea)`;
|
||||
updateMarkerPositions();
|
||||
}
|
||||
|
||||
function renderLostStation() {
|
||||
const station = lostStationGroup.selectAll('g.lost-station')
|
||||
.data(lostUsers.length > 0 ? [{count: lostUsers.length}] : []);
|
||||
|
||||
station.exit().remove();
|
||||
|
||||
const enter = station.enter()
|
||||
.append('g')
|
||||
.attr('class', 'lost-station')
|
||||
.style('cursor', 'pointer')
|
||||
.on("mouseover", function(event, d) {
|
||||
isHovering = true;
|
||||
tooltip.html(`<h3>🌊 Sea Station</h3><div class="info-row"><span class="highlight">${d.count}</span> Members drifting here...</div><div class="info-row" style="font-size:10px; color:#888;">(Update location in profile to be found)</div>`);
|
||||
tooltip.style("left", (event.pageX + 15) + "px").style("top", (event.pageY - 15) + "px").style("opacity", 1);
|
||||
})
|
||||
.on("mouseout", function() {
|
||||
isHovering = false;
|
||||
tooltip.style("opacity", 0);
|
||||
});
|
||||
|
||||
// Buoy/Station Visual
|
||||
enter.append('circle').attr('r', 12).attr('fill', 'rgba(46, 204, 113, 0.2)').attr('stroke', '#2ecc71').attr('stroke-width', 1).attr('stroke-dasharray', '2,2');
|
||||
enter.append('circle').attr('r', 4).attr('fill', '#2ecc71').attr('stroke', '#fff').attr('stroke-width', 1);
|
||||
|
||||
updateLostStationPosition();
|
||||
}
|
||||
|
||||
function showTooltip(event, d) {
|
||||
tooltip.html(`
|
||||
<h3>${d.displayName}</h3>
|
||||
@@ -601,9 +591,12 @@
|
||||
function updateMarkerPositions() {
|
||||
const markers = markerGroup.selectAll('circle.marker');
|
||||
if (markers.empty()) return;
|
||||
|
||||
const invCenter = projection.invert(center);
|
||||
|
||||
markers.each(function(d) {
|
||||
const coordinate = [d.longitude, d.latitude];
|
||||
const gdistance = d3.geoDistance(coordinate, projection.invert(center));
|
||||
const gdistance = d3.geoDistance(coordinate, invCenter);
|
||||
if (gdistance < 1.57) {
|
||||
const pos = projection(coordinate);
|
||||
d3.select(this).attr('cx', pos[0]).attr('cy', pos[1]).style('display', 'block');
|
||||
@@ -613,12 +606,31 @@
|
||||
});
|
||||
}
|
||||
|
||||
function updateLostStationPosition() {
|
||||
const station = lostStationGroup.selectAll('g.lost-station');
|
||||
if (station.empty()) return;
|
||||
|
||||
const invCenter = projection.invert(center);
|
||||
const coordinate = config.lostStationCoords;
|
||||
const gdistance = d3.geoDistance(coordinate, invCenter);
|
||||
|
||||
if (gdistance < 1.57) {
|
||||
const pos = projection(coordinate);
|
||||
station.attr('transform', `translate(${pos[0]},${pos[1]})`).style('display', 'block');
|
||||
} else {
|
||||
station.style('display', 'none');
|
||||
}
|
||||
}
|
||||
|
||||
function updateInstitutionPositions() {
|
||||
const instMarkers = institutionGroup.selectAll('g.institution-marker');
|
||||
if (instMarkers.empty()) return;
|
||||
|
||||
const invCenter = projection.invert(center);
|
||||
|
||||
instMarkers.each(function(d) {
|
||||
const coordinate = [d.longitude, d.latitude];
|
||||
const gdistance = d3.geoDistance(coordinate, projection.invert(center));
|
||||
const gdistance = d3.geoDistance(coordinate, invCenter);
|
||||
if (gdistance < 1.57) {
|
||||
const pos = projection(coordinate);
|
||||
d3.select(this).attr('transform', `translate(${pos[0]},${pos[1]})`).style('display', 'block');
|
||||
@@ -631,7 +643,6 @@
|
||||
// Animation
|
||||
function startAnimation() {
|
||||
d3.timer(function () {
|
||||
animateCloud(); // Always animate cloud
|
||||
if (!isDragging && !isPaused && !isHovering) {
|
||||
currentRotation[0] += config.speed;
|
||||
projection.rotate(currentRotation);
|
||||
@@ -641,14 +652,14 @@
|
||||
}
|
||||
|
||||
function redraw() {
|
||||
graticuleGroup.selectAll("path").attr("d", path);
|
||||
mapGroup.selectAll("path").attr("d", path);
|
||||
globeGroup.selectAll("path").attr("d", path);
|
||||
graticuleGroup.select("path").attr("d", path);
|
||||
mapGroup.select("path").attr("d", path);
|
||||
updateMarkerPositions();
|
||||
updateInstitutionPositions();
|
||||
updateLostStationPosition();
|
||||
}
|
||||
|
||||
// Input Handling (Drag, Resize, Search - Simplified for restore)
|
||||
// Input Handling
|
||||
const drag = d3.drag()
|
||||
.on("start", () => { isDragging = true; })
|
||||
.on("drag", (event) => {
|
||||
@@ -662,7 +673,6 @@
|
||||
|
||||
document.getElementById('rotation-toggle').addEventListener('click', () => { isPaused = !isPaused; });
|
||||
|
||||
// ... (Search logic would go here, kept largely as is in previous logic) ...
|
||||
const searchInput = document.getElementById('user-search');
|
||||
const searchDropdown = document.getElementById('search-dropdown');
|
||||
let debounceTimer;
|
||||
@@ -702,19 +712,10 @@
|
||||
svg.attr('width', w).attr('height', h);
|
||||
projection.translate([w/2, h/2]).scale(Math.min(w, h) / 2.5);
|
||||
center[0] = w/2; center[1] = h/2;
|
||||
redraw();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
// Handle Window Resize
|
||||
window.addEventListener('resize', () => {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
svg.attr('width', w).attr('height', h);
|
||||
projection.translate([w/2, h/2]).scale(Math.min(w, h) / 2.5);
|
||||
center[0] = w/2;
|
||||
center[1] = h/2;
|
||||
|
||||
// Update static globe background
|
||||
globeGroup.select("path").attr("d", path);
|
||||
|
||||
redraw();
|
||||
});
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
margin-bottom: 50px;
|
||||
width: 95%;
|
||||
max-width: 1200px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: #fff; /* Solid white background */
|
||||
border: 1px solid #333;
|
||||
box-shadow: 8px 8px 0px rgba(0,0,0,0.1);
|
||||
padding: 40px;
|
||||
@@ -150,9 +150,11 @@
|
||||
.legend {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
font-size: 0.8rem;
|
||||
font-family: 'Verdana', sans-serif;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
@@ -167,6 +169,104 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
border: 2px solid #333;
|
||||
box-shadow: 10px 10px 0px #ff6600;
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
animation: modalSlide 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlide {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 15px;
|
||||
color: #ff6600;
|
||||
border-bottom: 2px dashed #eee;
|
||||
padding-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.modal-details {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.show-more {
|
||||
font-size: 0.7rem;
|
||||
color: #ff6600;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-google {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background: #ff6600;
|
||||
color: white !important;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
margin-top: 25px;
|
||||
border: 2px solid #333;
|
||||
box-shadow: 4px 4px 0px #333;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.btn-google:hover {
|
||||
transform: translate(-2px, -2px);
|
||||
box-shadow: 6px 6px 0px #333;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 700px) {
|
||||
.day { min-height: 60px; }
|
||||
@@ -202,17 +302,45 @@
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #ff6600;"></div>
|
||||
<span>General Events</span>
|
||||
<div style="display: flex; gap: 20px;">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #ff6600;"></div>
|
||||
<span>General Events</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #00bcd4;"></div>
|
||||
<span>Historical Dates</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #8bc34a;"></div>
|
||||
<span>Community</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #00bcd4;"></div>
|
||||
<span>Historical Dates</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #8bc34a;"></div>
|
||||
<span>Community</span>
|
||||
<a href="https://calendar.google.com/calendar/embed?src=a6a109f7e2da7b66503ca3cd8ed1bc18c9755c0cecb99897a3761731f5663852%40group.calendar.google.com&ctz=America%2FNew_York" target="_blank" class="btn-google" style="margin-top: 20px; width: auto; display: inline-block; padding: 10px 20px;">
|
||||
VIEW FULL CALENDAR IN GOOGLE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Detail Modal -->
|
||||
<div id="eventModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<span class="modal-close" id="modalClose">×</span>
|
||||
<div id="modalTitle" class="modal-title">Event Title</div>
|
||||
<div class="modal-details">
|
||||
<div class="modal-row">
|
||||
<span class="modal-label">When</span>
|
||||
<span id="modalWhen"></span>
|
||||
</div>
|
||||
<div class="modal-row" id="locationRow">
|
||||
<span class="modal-label">Where</span>
|
||||
<span id="modalWhere"></span>
|
||||
</div>
|
||||
<div class="modal-row" id="descriptionRow">
|
||||
<span class="modal-label">Description</span>
|
||||
<div id="modalDescription"></div>
|
||||
</div>
|
||||
<a id="modalLink" href="#" target="_blank" class="btn-google">OPEN IN GOOGLE CALENDAR</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,49 +350,111 @@
|
||||
const monthNames = ["January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"];
|
||||
|
||||
const allEvents = [
|
||||
// December 2025 events (based on user's current context)
|
||||
// Static historical or community events
|
||||
const staticEvents = [
|
||||
{ date: new Date(2025, 11, 1), description: 'Tiny Torch Community is born', type: 'community' },
|
||||
{ date: new Date(2025, 11, 4), description: 'NeuralIPS 2025 Kickoff', type: 'general' },
|
||||
{ date: new Date(2025, 11, 8), description: 'Tiny Torch merged with MLSYSBOOK.ai', type: 'community' },
|
||||
{ date: new Date(2025, 11, 9), description: 'Tiny Torch official launch', type: 'community' },
|
||||
{ date: new Date(2025, 11, 10), description: 'Ada Lovelace Birthday (1815)', type: 'historical' },
|
||||
{ date: new Date(2025, 11, 17), description: 'MLSYSBOOK 10k GitHub Stars Celebration via Edge AI Foundation', type: 'community' },
|
||||
{ date: new Date(2025, 11, 17), description: 'MLSYSBOOK 10k GitHub Stars', type: 'community' },
|
||||
{ date: new Date(2025, 11, 25), description: 'AI Winter Solstice', type: 'general' },
|
||||
// Additional AI known dates (historical or general for other months)
|
||||
{ date: new Date(2022, 10, 30), description: 'ChatGPT Launched', type: 'historical' }, // November 30, 2022
|
||||
{ date: new Date(1956, 7, 1), description: 'Dartmouth Conference (Birth of AI)', type: 'historical' }, // August 1, 1956
|
||||
{ date: new Date(1997, 4, 11), description: 'Deep Blue vs Garry Kasparov', type: 'historical' }, // May 11, 1997
|
||||
{ date: new Date(2016, 2, 9), description: 'AlphaGo vs Lee Sedol', type: 'historical' }, // March 9, 2016
|
||||
{ date: new Date(2025, 0, 15), description: 'Future AI Summit', type: 'general' }, // January 15, 2025
|
||||
{ date: new Date(2025, 1, 20), description: 'Quantum AI Workshop', type: 'community' }, // February 20, 2025
|
||||
{ date: new Date(2025, 8, 1), description: 'Generative AI Conference', type: 'general' }, // Sep 1, 2025
|
||||
{ date: new Date(2025, 9, 26), description: 'AI Ethics Panel', type: 'community' } // Oct 26, 2025
|
||||
{ date: new Date(2022, 10, 30), description: 'ChatGPT Launched', type: 'historical' },
|
||||
{ date: new Date(1956, 7, 1), description: 'Dartmouth Conference', type: 'historical' },
|
||||
{ date: new Date(1997, 4, 11), description: 'Deep Blue vs Kasparov', type: 'historical' },
|
||||
{ date: new Date(2016, 2, 9), description: 'AlphaGo vs Lee Sedol', type: 'historical' }
|
||||
];
|
||||
|
||||
let currentYear = 2025; // Initial year (from context: Dec 1, 2025)
|
||||
let currentMonth = 11; // Initial month (December is 11)
|
||||
let fetchedEvents = [];
|
||||
|
||||
let today = new Date();
|
||||
if (today.getFullYear() < 2026) {
|
||||
today = new Date(2026, 1, 27);
|
||||
}
|
||||
|
||||
let currentYear = today.getFullYear();
|
||||
let currentMonth = today.getMonth();
|
||||
|
||||
async function fetchExternalEvents(year, month) {
|
||||
const timeMin = new Date(year, month, 1).toISOString();
|
||||
const timeMax = new Date(year, month + 1, 1).toISOString();
|
||||
const url = `https://ai-engineering-home.vercel.app/api/calendar?timeMin=${timeMin}&timeMax=${timeMax}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('API request failed');
|
||||
const data = await response.json();
|
||||
return (data.events || []).map(event => ({
|
||||
date: new Date(event.start),
|
||||
endDate: new Date(event.end),
|
||||
description: event.summary,
|
||||
fullDescription: event.description,
|
||||
location: event.location,
|
||||
type: 'general',
|
||||
link: event.htmlLink
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error fetching calendar events:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function openEventModal(event) {
|
||||
const modal = document.getElementById('eventModal');
|
||||
document.getElementById('modalTitle').textContent = event.description;
|
||||
|
||||
const startStr = event.date.toLocaleString([], { dateStyle: 'long', timeStyle: 'short' });
|
||||
const endStr = event.endDate ? ' to ' + event.endDate.toLocaleTimeString([], { timeStyle: 'short' }) : '';
|
||||
document.getElementById('modalWhen').textContent = startStr + endStr;
|
||||
|
||||
const locRow = document.getElementById('locationRow');
|
||||
if (event.location) {
|
||||
locRow.style.display = 'block';
|
||||
document.getElementById('modalWhere').textContent = event.location;
|
||||
} else {
|
||||
locRow.style.display = 'none';
|
||||
}
|
||||
|
||||
const descRow = document.getElementById('descriptionRow');
|
||||
if (event.fullDescription) {
|
||||
descRow.style.display = 'block';
|
||||
document.getElementById('modalDescription').innerHTML = event.fullDescription.replace(/\n/g, '<br>');
|
||||
} else {
|
||||
descRow.style.display = 'none';
|
||||
}
|
||||
|
||||
const linkBtn = document.getElementById('modalLink');
|
||||
if (event.link) {
|
||||
linkBtn.style.display = 'inline-block';
|
||||
linkBtn.href = event.link;
|
||||
} else {
|
||||
linkBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function getDaysInMonth(year, month) {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
}
|
||||
|
||||
function getFirstDayOfMonth(year, month) {
|
||||
// 0 = Sunday, 1 = Monday, etc.
|
||||
return new Date(year, month, 1).getDay();
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
async function renderCalendar() {
|
||||
const currentMonthYearSpan = document.getElementById('currentMonthYear');
|
||||
const calendarBody = document.getElementById('calendar-body');
|
||||
|
||||
currentMonthYearSpan.textContent = `${monthNames[currentMonth]} ${currentYear}`;
|
||||
calendarBody.innerHTML = ''; // Clear previous days
|
||||
|
||||
fetchedEvents = await fetchExternalEvents(currentYear, currentMonth);
|
||||
|
||||
calendarBody.innerHTML = '';
|
||||
|
||||
const daysInMonth = getDaysInMonth(currentYear, currentMonth);
|
||||
const firstDay = getFirstDayOfMonth(currentYear, currentMonth); // 0 for Sunday, 1 for Monday
|
||||
const firstDay = getFirstDayOfMonth(currentYear, currentMonth);
|
||||
|
||||
// Fill leading empty days (from previous month)
|
||||
const prevMonthDays = getDaysInMonth(currentYear, currentMonth - 1);
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
const dayDiv = document.createElement('div');
|
||||
@@ -273,35 +463,51 @@
|
||||
calendarBody.appendChild(dayDiv);
|
||||
}
|
||||
|
||||
// Fill current month days
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
const dayDiv = document.createElement('div');
|
||||
dayDiv.classList.add('day');
|
||||
dayDiv.innerHTML = `<div class="day-number">${i}</div>`;
|
||||
|
||||
// Add events for this day
|
||||
const dayEvents = allEvents.filter(event =>
|
||||
const combinedEvents = [...staticEvents, ...fetchedEvents];
|
||||
const dayEvents = combinedEvents.filter(event =>
|
||||
event.date.getFullYear() === currentYear &&
|
||||
event.date.getMonth() === currentMonth &&
|
||||
event.date.getDate() === i
|
||||
);
|
||||
|
||||
dayEvents.forEach(event => {
|
||||
const maxVisible = 2;
|
||||
dayEvents.slice(0, maxVisible).forEach(event => {
|
||||
const eventMarker = document.createElement('div');
|
||||
eventMarker.classList.add('event-marker');
|
||||
eventMarker.textContent = event.description;
|
||||
if (event.type) {
|
||||
eventMarker.classList.add(event.type);
|
||||
}
|
||||
if (event.type) eventMarker.classList.add(event.type);
|
||||
eventMarker.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
openEventModal(event);
|
||||
};
|
||||
dayDiv.appendChild(eventMarker);
|
||||
});
|
||||
|
||||
if (dayEvents.length > maxVisible) {
|
||||
const more = document.createElement('div');
|
||||
more.classList.add('show-more');
|
||||
more.textContent = `+ ${dayEvents.length - maxVisible} more`;
|
||||
more.onclick = (e) => {
|
||||
// For simplicity, just show the third event in the modal
|
||||
// or could list them all. Here we'll just open the modal for the first one
|
||||
// and let the user navigate or list them in the modal.
|
||||
// Better: open the modal with a list if clicked "more".
|
||||
// For now, let's just make the whole day clickable if there's more.
|
||||
openEventModal(dayEvents[maxVisible]);
|
||||
};
|
||||
dayDiv.appendChild(more);
|
||||
}
|
||||
|
||||
calendarBody.appendChild(dayDiv);
|
||||
}
|
||||
|
||||
// Fill trailing empty days (from next month)
|
||||
const totalDaysRendered = firstDay + daysInMonth;
|
||||
const remainingCells = 42 - totalDaysRendered; // Max 6 rows * 7 days = 42 cells
|
||||
const remainingCells = 42 - totalDaysRendered;
|
||||
for (let i = 1; i <= remainingCells; i++) {
|
||||
const dayDiv = document.createElement('div');
|
||||
dayDiv.classList.add('day', 'other-month');
|
||||
@@ -310,7 +516,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function changeMonth(delta) {
|
||||
async function changeMonth(delta) {
|
||||
currentMonth += delta;
|
||||
if (currentMonth < 0) {
|
||||
currentMonth = 11;
|
||||
@@ -319,16 +525,20 @@
|
||||
currentMonth = 0;
|
||||
currentYear++;
|
||||
}
|
||||
renderCalendar();
|
||||
await renderCalendar();
|
||||
}
|
||||
|
||||
// Initial render
|
||||
renderCalendar();
|
||||
|
||||
// Event listeners for navigation buttons
|
||||
document.getElementById('prevMonth').addEventListener('click', () => changeMonth(-1));
|
||||
document.getElementById('nextMonth').addEventListener('click', () => changeMonth(1));
|
||||
|
||||
document.getElementById('modalClose').addEventListener('click', () => {
|
||||
document.getElementById('eventModal').style.display = 'none';
|
||||
});
|
||||
window.addEventListener('click', (e) => {
|
||||
const modal = document.getElementById('eventModal');
|
||||
if (e.target === modal) modal.style.display = 'none';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { NETLIFY_URL, SUPABASE_URL, SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, getBasePath } from './config.js';
|
||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||
import { updateNavState } from './ui.js?v=2';
|
||||
import { updateNavState } from './ui.js';
|
||||
import { closeProfileModal, openProfileModal } from './profile.js';
|
||||
import { getSession, forceLogin, clearSession } from './state.js?v=2';
|
||||
import { getSession, forceLogin, clearSession, supabase } from './state.js';
|
||||
|
||||
// Initialize Supabase Client
|
||||
const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY);
|
||||
export { supabase };
|
||||
|
||||
export async function signInWithSocial(provider) {
|
||||
@@ -267,20 +264,51 @@ export async function handleAuth(e) {
|
||||
authSubmit.innerHTML = '<div class="spinner"></div>';
|
||||
|
||||
try {
|
||||
let endpoint, body;
|
||||
|
||||
if (currentMode === 'forgot') {
|
||||
endpoint = '/api/auth/reset-password';
|
||||
body = { email };
|
||||
} else {
|
||||
endpoint = currentMode === 'login' ? '/api/auth/login' : '/api/auth/signup';
|
||||
body = {
|
||||
// Direct Supabase flow for Signup
|
||||
if (currentMode === 'signup') {
|
||||
const redirectUrl = window.location.origin + basePath + '/index.html?action=login&confirmed_email=true';
|
||||
console.log("Requesting signup with redirect to:", redirectUrl);
|
||||
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
redirect_to: window.location.origin + basePath + '/index.html?action=login&confirmed_email=true'
|
||||
};
|
||||
options: {
|
||||
emailRedirectTo: redirectUrl
|
||||
}
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
closeModal();
|
||||
showMessageModal(
|
||||
'Check your Email',
|
||||
'If you don\'t already have an account, we have sent you an email. Please check your inbox to confirm your signup.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct Supabase flow for Password Reset
|
||||
if (currentMode === 'forgot') {
|
||||
const redirectUrl = window.location.origin + basePath + '/index.html?action=reset-password';
|
||||
console.log("Requesting password reset with redirect to:", redirectUrl);
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: redirectUrl
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
showMessageModal('Reset Link Sent', 'If an account exists, a reset link has been sent.');
|
||||
setMode('login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep existing API flow for Login
|
||||
const endpoint = '/api/auth/login';
|
||||
const body = {
|
||||
email,
|
||||
password,
|
||||
redirect_to: window.location.origin + basePath + '/index.html?action=login&confirmed_email=true'
|
||||
};
|
||||
|
||||
const url = `${NETLIFY_URL}${endpoint}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -294,72 +322,60 @@ export async function handleAuth(e) {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (currentMode === 'forgot') {
|
||||
if (response.ok) {
|
||||
showMessageModal('Reset Link Sent', data.message || 'If an account exists, a reset link has been sent.');
|
||||
setMode('login');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to send reset link');
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Login failed');
|
||||
}
|
||||
|
||||
if (data.access_token) {
|
||||
localStorage.setItem("tinytorch_token", data.access_token);
|
||||
if (data.refresh_token) localStorage.setItem("tinytorch_refresh_token", data.refresh_token);
|
||||
localStorage.setItem("tinytorch_user", JSON.stringify(data.user));
|
||||
|
||||
// Sync Supabase Client so it doesn't trigger SIGNED_OUT
|
||||
if (data.refresh_token) {
|
||||
const { error: sessionError } = await supabase.auth.setSession({
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token
|
||||
});
|
||||
if (sessionError) {
|
||||
console.error("Supabase setSession error during login:", sessionError);
|
||||
}
|
||||
} else {
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || (currentMode === 'login' ? 'Login failed' : 'Signup failed'));
|
||||
}
|
||||
|
||||
if (currentMode === 'login') {
|
||||
if (data.access_token) {
|
||||
localStorage.setItem("tinytorch_token", data.access_token);
|
||||
if (data.refresh_token) localStorage.setItem("tinytorch_refresh_token", data.refresh_token);
|
||||
localStorage.setItem("tinytorch_user", JSON.stringify(data.user));
|
||||
updateNavState();
|
||||
|
||||
// Sync Supabase Client so it doesn't trigger SIGNED_OUT
|
||||
if (data.refresh_token) {
|
||||
const { error: sessionError } = await supabase.auth.setSession({
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token
|
||||
});
|
||||
if (sessionError) {
|
||||
console.error("Supabase setSession error during login:", sessionError);
|
||||
}
|
||||
}
|
||||
// Check Profile Completeness immediately
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('display_name, institution, location')
|
||||
.eq('id', data.user.id)
|
||||
.single();
|
||||
|
||||
updateNavState();
|
||||
const hasName = profile && profile.display_name;
|
||||
const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution);
|
||||
const hasLoc = profile && profile.location;
|
||||
|
||||
// Check Profile Completeness immediately
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('display_name, institution, location')
|
||||
.eq('id', data.user.id)
|
||||
.single();
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const nextParam = params.get('next');
|
||||
|
||||
closeModal(); // Always close modal on success
|
||||
|
||||
const hasName = profile && profile.display_name;
|
||||
const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution);
|
||||
const hasLoc = profile && profile.location;
|
||||
if (nextParam) {
|
||||
// Ensure nextParam is a clean path if it was encoded
|
||||
const cleanNext = decodeURIComponent(nextParam).split('?')[0];
|
||||
window.location.href = cleanNext;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasName || !hasInst || !hasLoc) {
|
||||
window.location.href = basePath + '/profile_setup.html';
|
||||
return;
|
||||
}
|
||||
if (!hasName || !hasInst || !hasLoc) {
|
||||
window.location.href = basePath + '/profile_setup.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('action') === 'profile') {
|
||||
closeModal();
|
||||
openProfileModal();
|
||||
} else {
|
||||
window.location.href = basePath + '/dashboard.html';
|
||||
}
|
||||
}
|
||||
if (params.get('action') === 'profile') {
|
||||
openProfileModal();
|
||||
} else {
|
||||
// Signup Success - Show Message Modal
|
||||
// We close the auth modal first so it doesn't overlap
|
||||
closeModal();
|
||||
showMessageModal(
|
||||
'Check your Email',
|
||||
'If you don\'t already have an account, we have sent you an email. Please check your inbox to confirm your signup.',
|
||||
() => {
|
||||
window.location.href = basePath + '/dashboard.html';
|
||||
}
|
||||
);
|
||||
window.location.href = basePath + '/dashboard.html';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
// --- CANDLE ANIMATION MODULE ---
|
||||
const activeLoops = new Set();
|
||||
|
||||
export function initCandle(canvasId) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
|
||||
// Prevent multiple loops on the same canvas
|
||||
if (activeLoops.has(canvasId)) {
|
||||
console.log(`Candle loop already running for ${canvasId}, skipping init.`);
|
||||
return;
|
||||
}
|
||||
activeLoops.add(canvasId);
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Coordinate system setup:
|
||||
|
||||
@@ -1,81 +1,92 @@
|
||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||
import { SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, SUPABASE_URL, getBasePath } from './config.js';
|
||||
import { clearSession, supabase } from './state.js';
|
||||
|
||||
const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY);
|
||||
const LOGIN_PAGE = getBasePath() + '/index.html';
|
||||
const SETUP_PAGE = getBasePath() + '/profile_setup.html';
|
||||
const DASHBOARD_PAGE = getBasePath() + '/dashboard.html';
|
||||
|
||||
(async function guard() {
|
||||
// 0. HYDRATE SESSION (Fix for Direct Email Login)
|
||||
const path = window.location.pathname;
|
||||
// Enhanced path checking for landing page
|
||||
const isOnIndex = path.endsWith('/') || path.endsWith('index.html') || path === getBasePath() || path === getBasePath() + '/';
|
||||
const isOnSetupPage = path.includes('profile_setup.html');
|
||||
const isPublicPage = isOnIndex || path.includes('login') || path.includes('about') || path.includes('contact');
|
||||
const isProtected = !isPublicPage;
|
||||
|
||||
// 0. Get Session
|
||||
const storedToken = localStorage.getItem("tinytorch_token");
|
||||
const storedRefresh = localStorage.getItem("tinytorch_refresh_token");
|
||||
|
||||
if (storedToken && storedRefresh) {
|
||||
await supabase.auth.setSession({
|
||||
access_token: storedToken,
|
||||
refresh_token: storedRefresh
|
||||
});
|
||||
try {
|
||||
await supabase.auth.setSession({
|
||||
access_token: storedToken,
|
||||
refresh_token: storedRefresh
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Guard: setSession failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Check Session (Supabase Client)
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
let profile = null;
|
||||
|
||||
if (session) {
|
||||
// 2a. Fetch Profile via Client
|
||||
const { data } = await supabase
|
||||
.from('profiles')
|
||||
.select('display_name, institution, location')
|
||||
.eq('id', session.user.id)
|
||||
.single();
|
||||
profile = data;
|
||||
} else {
|
||||
// 1b. Fallback: Check Token Manually
|
||||
if (!storedToken) {
|
||||
// No session, no token -> Redirect
|
||||
if (!window.location.pathname.includes('index') && !window.location.pathname.includes('login') && !window.location.pathname.includes('about')) {
|
||||
window.location.href = LOGIN_PAGE + '?action=login&next=' + encodeURIComponent(window.location.pathname);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Have token, verify via API
|
||||
try {
|
||||
const res = await fetch(`${SUPABASE_URL}/get-profile-details`, {
|
||||
headers: { 'Authorization': `Bearer ${storedToken}` }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Token invalid");
|
||||
}
|
||||
const data = await res.json();
|
||||
profile = data.profile; // API returns { profile: {...}, completed_modules: [...] }
|
||||
|
||||
} catch (e) {
|
||||
console.warn("Guard: Token validation failed", e);
|
||||
if (!window.location.pathname.includes('index') && !window.location.pathname.includes('login')) {
|
||||
window.location.href = LOGIN_PAGE + '?action=login&next=' + encodeURIComponent(window.location.pathname);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!session && isProtected) {
|
||||
console.log("🚧 No session on protected page. Redirecting to login...");
|
||||
window.location.href = LOGIN_PAGE + '?action=login&next=' + encodeURIComponent(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. The Rules
|
||||
// Must have ALL three: Name, Institution, Location
|
||||
if (!session) return; // Public page, no session, we are fine.
|
||||
|
||||
// 1. Fetch Profile with Timeout
|
||||
let profile = null;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
try {
|
||||
// Use the API for profile details
|
||||
const res = await fetch(`${SUPABASE_URL}/get-profile-details`, {
|
||||
headers: { 'Authorization': `Bearer ${session.access_token}` },
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
profile = data.profile;
|
||||
} else if (res.status === 401 || res.status === 404) {
|
||||
// 401: Token expired, 404: User deleted from DB
|
||||
console.warn(`Guard: Session invalid or account deleted (${res.status}). Purging...`);
|
||||
await clearSession();
|
||||
|
||||
if (isProtected) {
|
||||
window.location.href = LOGIN_PAGE + '?action=login';
|
||||
} else {
|
||||
// If on a public page, just reload to clear UI state
|
||||
window.location.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Guard: Profile fetch failed or timed out", e);
|
||||
}
|
||||
|
||||
// 2. The Rules
|
||||
const hasName = profile && profile.display_name;
|
||||
const hasInst = profile && profile.institution && (Array.isArray(profile.institution) ? profile.institution.length > 0 : !!profile.institution);
|
||||
const hasLoc = profile && profile.location;
|
||||
|
||||
const isComplete = hasName && hasInst && hasLoc;
|
||||
const isOnSetupPage = window.location.pathname.includes('profile_setup');
|
||||
|
||||
if (!isComplete && !isOnSetupPage) {
|
||||
console.log("🚧 Profile incomplete. Redirecting to setup...");
|
||||
window.location.href = SETUP_PAGE;
|
||||
}
|
||||
else if (isComplete && isOnSetupPage) {
|
||||
// If they are done but try to visit setup, send them to dashboard
|
||||
window.location.href = getBasePath() + '/dashboard.html';
|
||||
if (isComplete) {
|
||||
if (isOnSetupPage) {
|
||||
console.log("✅ Profile complete. Moving to dashboard...");
|
||||
window.location.href = DASHBOARD_PAGE;
|
||||
}
|
||||
} else {
|
||||
if (!isOnSetupPage && isProtected) {
|
||||
console.log("🚧 Profile incomplete. Redirecting to setup...");
|
||||
window.location.href = SETUP_PAGE;
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SUPABASE_URL, NETLIFY_URL, getBasePath } from './config.js';
|
||||
import { forceLogin } from './state.js?v=2';
|
||||
import { forceLogin, getSession, clearSession } from './state.js';
|
||||
import { initCandle } from './candle.js';
|
||||
|
||||
export async function geocodeAndSetCoordinates(location) {
|
||||
@@ -39,6 +39,21 @@ let candleInitialized = false;
|
||||
export function openProfileModal() {
|
||||
const profileOverlay = document.getElementById('profileOverlay');
|
||||
profileOverlay.classList.add('active');
|
||||
|
||||
// Reset delete section
|
||||
const deleteBtn = document.getElementById('profileDeleteBtn');
|
||||
const deleteConfirmSection = document.getElementById('deleteConfirmSection');
|
||||
const deleteConfirmInput = document.getElementById('profileDeleteConfirmInput');
|
||||
const deleteFinalBtn = document.getElementById('profileDeleteFinalBtn');
|
||||
if (deleteBtn) deleteBtn.classList.remove('hidden');
|
||||
if (deleteConfirmSection) deleteConfirmSection.classList.add('hidden');
|
||||
if (deleteConfirmInput) deleteConfirmInput.value = '';
|
||||
if (deleteFinalBtn) {
|
||||
deleteFinalBtn.disabled = true;
|
||||
deleteFinalBtn.style.opacity = '0.5';
|
||||
deleteFinalBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
|
||||
fetchUserProfile();
|
||||
|
||||
if (!candleInitialized) {
|
||||
@@ -49,6 +64,77 @@ export function openProfileModal() {
|
||||
}
|
||||
}
|
||||
|
||||
export function setupProfileDeleteEvents() {
|
||||
const deleteBtn = document.getElementById('profileDeleteBtn');
|
||||
const deleteConfirmSection = document.getElementById('deleteConfirmSection');
|
||||
const deleteConfirmName = document.getElementById('deleteConfirmName');
|
||||
const deleteConfirmInput = document.getElementById('profileDeleteConfirmInput');
|
||||
const deleteFinalBtn = document.getElementById('profileDeleteFinalBtn');
|
||||
const profileDisplayNameInput = document.getElementById('profileDisplayName');
|
||||
|
||||
if (!deleteBtn) return;
|
||||
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
let displayName = profileDisplayNameInput.value.trim();
|
||||
if (!displayName) {
|
||||
const { email } = getSession();
|
||||
displayName = email || 'DELETE';
|
||||
}
|
||||
deleteConfirmName.textContent = displayName;
|
||||
deleteConfirmSection.classList.remove('hidden');
|
||||
deleteBtn.classList.add('hidden');
|
||||
});
|
||||
|
||||
deleteConfirmInput.addEventListener('input', () => {
|
||||
const displayName = deleteConfirmName.textContent.trim();
|
||||
if (deleteConfirmInput.value.trim() === displayName) {
|
||||
deleteFinalBtn.disabled = false;
|
||||
deleteFinalBtn.style.opacity = '1';
|
||||
deleteFinalBtn.style.cursor = 'pointer';
|
||||
} else {
|
||||
deleteFinalBtn.disabled = true;
|
||||
deleteFinalBtn.style.opacity = '0.5';
|
||||
deleteFinalBtn.style.cursor = 'not-allowed';
|
||||
}
|
||||
});
|
||||
|
||||
deleteFinalBtn.addEventListener('click', async () => {
|
||||
const confirmResult = confirm("Are you absolutely sure? This will delete all your data and access.");
|
||||
if (!confirmResult) return;
|
||||
|
||||
deleteFinalBtn.disabled = true;
|
||||
deleteFinalBtn.textContent = 'Deleting...';
|
||||
|
||||
const token = localStorage.getItem("tinytorch_token");
|
||||
try {
|
||||
const response = await fetch(`${SUPABASE_URL}/delete-account`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errData = {};
|
||||
try {
|
||||
errData = await response.json();
|
||||
} catch(e) {}
|
||||
throw new Error(errData.error || 'Failed to delete account');
|
||||
}
|
||||
|
||||
alert("Your account has been deleted.");
|
||||
await clearSession();
|
||||
window.location.href = getBasePath() + '/index.html';
|
||||
} catch (error) {
|
||||
console.error("Delete account error:", error);
|
||||
alert("Error deleting account: " + error.message);
|
||||
deleteFinalBtn.disabled = false;
|
||||
deleteFinalBtn.textContent = 'Permanently Delete My Account';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function closeProfileModal() {
|
||||
const profileOverlay = document.getElementById('profileOverlay');
|
||||
profileOverlay.classList.remove('active');
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { getBasePath } from './config.js';
|
||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||
import { SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY } from './config.js';
|
||||
|
||||
const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, {
|
||||
auth: {
|
||||
flowType: 'pkce', // Prefer PKCE for security, or keep 'implicit' if standard for this app
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: true
|
||||
}
|
||||
});
|
||||
export { supabase };
|
||||
|
||||
// State Management
|
||||
export function getSession() {
|
||||
@@ -12,10 +24,46 @@ export function getSession() {
|
||||
return { token, email, isLoggedIn: !!token };
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
export async function clearSession() {
|
||||
console.log("🧹 Aggressively clearing session and cookies...");
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} catch (e) {
|
||||
console.warn("Supabase signOut error during clearSession:", e);
|
||||
}
|
||||
|
||||
// 1. Explicitly remove our own keys
|
||||
localStorage.removeItem("tinytorch_token");
|
||||
localStorage.removeItem("tinytorch_refresh_token");
|
||||
localStorage.removeItem("tinytorch_user");
|
||||
sessionStorage.removeItem("tinytorch_location_checked");
|
||||
|
||||
// 2. Clear all Supabase and auth-related keys from localStorage
|
||||
const keysToRemove = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && (
|
||||
key.includes("supabase") ||
|
||||
key.includes("auth-token") ||
|
||||
key.startsWith("sb-")
|
||||
)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(k => localStorage.removeItem(k));
|
||||
|
||||
// 3. Clear all auth-related cookies
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i];
|
||||
const eqPos = cookie.indexOf("=");
|
||||
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
|
||||
// Clear for all common paths and subdomains
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=" + window.location.hostname;
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
||||
}
|
||||
|
||||
console.log("✨ Session cleared.");
|
||||
}
|
||||
|
||||
export function forceLogin() {
|
||||
|
||||
@@ -441,6 +441,33 @@ export function injectStyles() {
|
||||
background: #b71c1c;
|
||||
}
|
||||
|
||||
.profile-delete-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
color: #d32f2f;
|
||||
border: 1px solid #d32f2f;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.profile-delete-btn:hover:not(:disabled) {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
#profileDeleteFinalBtn {
|
||||
background: #d32f2f;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#profileDeleteFinalBtn:hover:not(:disabled) {
|
||||
background: #b71c1c;
|
||||
}
|
||||
|
||||
/* --- Mobile Optimizations --- */
|
||||
@media (max-width: 768px) {
|
||||
/* Sidebar becomes a bottom sheet */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBasePath, NETLIFY_URL } from './config.js';
|
||||
import { getSession } from './state.js?v=2';
|
||||
import { getSession } from './state.js';
|
||||
|
||||
export function updateNavState() {
|
||||
const { isLoggedIn, email: userEmail } = getSession();
|
||||
@@ -132,15 +132,15 @@ export function renderLayout() {
|
||||
<div class="profile-overlay" id="profileOverlay">
|
||||
<div class="profile-modal" style="max-width: 850px; width: 95%;">
|
||||
<button class="profile-close" id="profileClose">×</button>
|
||||
<div style="display: flex; gap: 30px; align-items: stretch;">
|
||||
<div style="flex: 1;">
|
||||
<h2 class="profile-title">Your Profile</h2>
|
||||
<div style="display: flex; gap: 40px; align-items: flex-start; position: relative;">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<h2 class="profile-title" style="text-align: left; margin-bottom: 25px;">Your Profile</h2>
|
||||
<form id="profileForm">
|
||||
<div class="profile-form-group">
|
||||
<label for="profileDisplayName" class="profile-label">Display Name:</label>
|
||||
<input type="text" class="profile-input" id="profileDisplayName" placeholder="Display Name">
|
||||
</div>
|
||||
|
||||
<!-- ... rest of the form ... -->
|
||||
<div class="profile-form-group">
|
||||
<label for="profileFullName" class="profile-label">Full Name:</label>
|
||||
<input type="text" class="profile-input" id="profileFullName" placeholder="Your Full Name">
|
||||
@@ -180,12 +180,26 @@ export function renderLayout() {
|
||||
|
||||
<button type="submit" class="profile-submit" id="profileSubmit">Update Profile</button>
|
||||
<button type="button" class="profile-logout-btn" id="profileLogoutBtn">Logout</button>
|
||||
|
||||
<div class="profile-danger-zone" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #fee2e2;">
|
||||
<h3 style="color: #d32f2f; font-size: 1rem; margin-bottom: 10px;">Danger Zone</h3>
|
||||
<p style="font-size: 0.85rem; color: #666; margin-bottom: 15px;">Deleting your account is permanent and cannot be undone.</p>
|
||||
<button type="button" class="profile-delete-btn" id="profileDeleteBtn">Delete Account</button>
|
||||
|
||||
<div id="deleteConfirmSection" class="hidden" style="margin-top: 15px; padding: 15px; background: #fff5f5; border-radius: 10px; border: 1px solid #fecaca;">
|
||||
<p style="font-size: 0.85rem; color: #b91c1c; margin-bottom: 10px;">Type your display name <strong id="deleteConfirmName"></strong> to confirm:</p>
|
||||
<input type="text" class="profile-input" id="profileDeleteConfirmInput" placeholder="Enter display name">
|
||||
<button type="button" class="profile-delete-btn" id="profileDeleteFinalBtn" style="margin-top: 10px; opacity: 0.5; cursor: not-allowed;" disabled>Permanently Delete My Account</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Flame Side -->
|
||||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 180px; border-left: 1px solid #f0f0f0; background: rgba(252, 252, 252, 0.5);">
|
||||
<canvas id="profileCandleCanvas" width="16" height="24" style="width: 150px; height: auto; image-rendering: pixelated; filter: drop-shadow(4px 4px 0px rgba(0,0,0,0.05));"></canvas>
|
||||
<div style="flex: 0 0 220px; min-height: 450px; border-left: 1px solid #f0f0f0; background: rgba(252, 252, 252, 0.5); border-radius: 0 16px 16px 0; align-self: stretch; position: relative;">
|
||||
<div style="position: sticky; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px;">
|
||||
<canvas id="profileCandleCanvas" width="16" height="24" style="width: 140px; height: auto; image-rendering: pixelated; filter: drop-shadow(0 10px 20px rgba(255,102,0,0.2));"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
78
tinytorch/site/extra/community/package-lock.json
generated
Normal file
78
tinytorch/site/extra/community/package-lock.json
generated
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "tinytorch-community-tests",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tinytorch-community-tests",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
tinytorch/site/extra/community/package.json
Normal file
12
tinytorch/site/extra/community/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "tinytorch-community-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "E2E tests for Tiny Torch Community",
|
||||
"scripts": {
|
||||
"test": "npx playwright test",
|
||||
"test:ui": "npx playwright test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.42.0"
|
||||
}
|
||||
}
|
||||
21
tinytorch/site/extra/community/playwright.config.js
Normal file
21
tinytorch/site/extra/community/playwright.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { defineConfig, devices } = require('@playwright/test');
|
||||
|
||||
module.exports = defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:8000/community/',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -307,9 +307,10 @@
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm';
|
||||
import { SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, SUPABASE_URL, getBasePath } from './modules/config.js';
|
||||
import { initCandle } from './modules/candle.js';
|
||||
import { supabase } from './modules/state.js';
|
||||
import './modules/guard.js'; // Guard handles access control and completeness redirects
|
||||
|
||||
// Initialize Candle
|
||||
initCandle('candleCanvas');
|
||||
@@ -380,65 +381,48 @@
|
||||
const depthSegments = 100;
|
||||
const geometry = new THREE.PlaneGeometry(width, depth, widthSegments, depthSegments);
|
||||
geometry.rotateX(-Math.PI / 2);
|
||||
const count = geometry.attributes.position.count;
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(count * 3), 3));
|
||||
const material = new THREE.MeshBasicMaterial({ vertexColors: true, wireframe: true, transparent: true, opacity: 0.4 });
|
||||
const terrain = new THREE.Mesh(geometry, material);
|
||||
|
||||
const terrainMat = new THREE.ShaderMaterial({
|
||||
vertexShader: document.getElementById('terrain-vs').textContent,
|
||||
fragmentShader: document.getElementById('terrain-fs').textContent,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uNoiseScale: { value: 0.15 },
|
||||
uHeightScale: { value: 4.0 }
|
||||
},
|
||||
wireframe: true,
|
||||
transparent: true
|
||||
});
|
||||
|
||||
const terrain = new THREE.Mesh(geometry, terrainMat);
|
||||
scene.add(terrain);
|
||||
|
||||
const solidMat = new THREE.MeshBasicMaterial({ color: 0xf5f5f0, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
|
||||
// Solid base layer
|
||||
const solidMat = new THREE.ShaderMaterial({
|
||||
vertexShader: document.getElementById('terrain-vs').textContent,
|
||||
fragmentShader: document.getElementById('terrain-fs').textContent,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uNoiseScale: { value: 0.15 },
|
||||
uHeightScale: { value: 4.0 }
|
||||
},
|
||||
polygonOffset: true,
|
||||
polygonOffsetFactor: 1,
|
||||
polygonOffsetUnits: 1
|
||||
});
|
||||
const terrainSolid = new THREE.Mesh(geometry, solidMat);
|
||||
scene.add(terrainSolid);
|
||||
|
||||
const noiseScale = 0.15;
|
||||
const heightScale = 4.0;
|
||||
const positionAttribute = geometry.attributes.position;
|
||||
const colorAttribute = geometry.attributes.color;
|
||||
const originalPositions = positionAttribute.array.slice();
|
||||
|
||||
const colorWater = new THREE.Color(0x3498db);
|
||||
const colorSand = new THREE.Color(0xf1c40f);
|
||||
const colorGrass = new THREE.Color(0x2ecc71);
|
||||
const colorRock = new THREE.Color(0xe74c3c);
|
||||
|
||||
function getColorForHeight(h) {
|
||||
let finalColor = new THREE.Color();
|
||||
if (h < -0.2) finalColor.copy(colorWater);
|
||||
else if (h < 0.1) finalColor.lerpColors(colorWater, colorSand, (h + 0.2) / 0.3);
|
||||
else if (h < 0.6) finalColor.lerpColors(colorSand, colorGrass, (h - 0.1) / 0.5);
|
||||
else finalColor.lerpColors(colorGrass, colorRock, Math.min((h - 0.6) / 0.9, 1));
|
||||
return finalColor;
|
||||
}
|
||||
|
||||
function updateScene() {
|
||||
skyMesh.material.uniforms.uTime.value += 0.01;
|
||||
const time = skyMesh.material.uniforms.uTime.value;
|
||||
const flyOverY = time * 2; // Simpler flyover
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ix = i * 3;
|
||||
const iz = i * 3 + 2;
|
||||
const x = originalPositions[ix];
|
||||
const z = originalPositions[iz];
|
||||
const vZ = z - flyOverY;
|
||||
const noiseVal = Noise.perlin2(x * noiseScale, vZ * noiseScale);
|
||||
let height = noiseVal * heightScale;
|
||||
if (height < -1) height = -1;
|
||||
positionAttribute.setY(i, height);
|
||||
const c = getColorForHeight(noiseVal);
|
||||
c.multiplyScalar(0.9);
|
||||
colorAttribute.setXYZ(i, c.r, c.g, c.b);
|
||||
}
|
||||
positionAttribute.needsUpdate = true;
|
||||
colorAttribute.needsUpdate = true;
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
updateScene();
|
||||
const time = performance.now() * 0.001;
|
||||
skyMesh.material.uniforms.uTime.value = time;
|
||||
terrain.material.uniforms.uTime.value = time;
|
||||
terrainSolid.material.uniforms.uTime.value = time;
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
@@ -447,17 +431,7 @@
|
||||
|
||||
|
||||
// --- AUTH & PROFILE LOGIC ---
|
||||
const supabase = createClient(SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY);
|
||||
|
||||
// --- 0. HYDRATE SESSION ---
|
||||
const storedToken = localStorage.getItem("tinytorch_token");
|
||||
const storedRefresh = localStorage.getItem("tinytorch_refresh_token");
|
||||
if (storedToken && storedRefresh) {
|
||||
await supabase.auth.setSession({
|
||||
access_token: storedToken,
|
||||
refresh_token: storedRefresh
|
||||
});
|
||||
}
|
||||
// supabase is imported from state.js above
|
||||
|
||||
// --- 1. AUTOCOMPLETE LOGIC ---
|
||||
const locInput = document.getElementById('locationInput');
|
||||
@@ -491,63 +465,27 @@
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// --- 2. PRE-FILL LOGIC (WITH FALLBACK) ---
|
||||
async function loadUser() {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
let userProfile = null;
|
||||
let userId = null;
|
||||
// --- 2. PRE-FILL LOGIC ---
|
||||
async function loadUser(user) {
|
||||
if (!user) return;
|
||||
|
||||
if(!user) {
|
||||
// Fallback: Check Token Manually if getUser failed (e.g. missing refresh token)
|
||||
if (storedToken) {
|
||||
try {
|
||||
// Fetch details directly to confirm token and get user ID
|
||||
const res = await fetch(`${SUPABASE_URL}/get-profile-details`, {
|
||||
headers: { 'Authorization': `Bearer ${storedToken}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Construct a minimal user object or just use the profile data
|
||||
// We need userId to query 'profiles' or we can just use the data returned
|
||||
userProfile = data.profile;
|
||||
userId = data.profile.id; // Assuming profile has ID, or we get it from token decoding (complicated without lib)
|
||||
// Actually, get-profile-details returns the profile! We can use it directly.
|
||||
} else {
|
||||
// Token invalid
|
||||
window.location.href = getBasePath() + '/index.html';
|
||||
return;
|
||||
}
|
||||
} catch(e) {
|
||||
window.location.href = getBasePath() + '/index.html';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
window.location.href = getBasePath() + '/index.html';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
userId = user.id;
|
||||
}
|
||||
|
||||
// 1. Try to grab name from Google/GitHub metadata (if available from getUser)
|
||||
if(user && user.user_metadata) {
|
||||
// 1. Try to grab name from Google/GitHub metadata
|
||||
if(user.user_metadata) {
|
||||
if(user.user_metadata.full_name) {
|
||||
document.getElementById('fullName').value = user.user_metadata.full_name;
|
||||
const parts = user.user_metadata.full_name.split(' ');
|
||||
if(parts.length > 0) document.getElementById('displayName').value = parts[0];
|
||||
if(parts.length > 0 && !document.getElementById('displayName').value) {
|
||||
document.getElementById('displayName').value = parts[0];
|
||||
}
|
||||
}
|
||||
if(user.user_metadata.name && !document.getElementById('fullName').value) {
|
||||
document.getElementById('fullName').value = user.user_metadata.name;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Load Profile Data (Either from fallback or DB)
|
||||
if (userProfile) {
|
||||
populateForm(userProfile);
|
||||
} else if (userId) {
|
||||
const { data: profile } = await supabase.from('profiles').select('*').eq('id', userId).single();
|
||||
if(profile) populateForm(profile);
|
||||
}
|
||||
// 2. Load Profile Data from DB
|
||||
const { data: profile } = await supabase.from('profiles').select('*').eq('id', user.id).single();
|
||||
if(profile) populateForm(profile);
|
||||
}
|
||||
|
||||
function populateForm(profile) {
|
||||
@@ -572,7 +510,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
loadUser();
|
||||
// Listen for auth state to trigger load
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (session && session.user) {
|
||||
loadUser(session.user);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial load check
|
||||
supabase.auth.getUser().then(({ data: { user } }) => {
|
||||
if (user) loadUser(user);
|
||||
});
|
||||
|
||||
// --- 3. SUBMIT LOGIC ---
|
||||
document.getElementById('setupForm').addEventListener('submit', async (e) => {
|
||||
@@ -582,8 +530,11 @@
|
||||
btn.disabled = true;
|
||||
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
// Use stored token if session is missing (Fallback)
|
||||
const tokenToUse = session ? session.access_token : storedToken;
|
||||
if (!session) {
|
||||
alert("Session lost. Please log in again.");
|
||||
window.location.href = getBasePath() + '/index.html?action=login';
|
||||
return;
|
||||
}
|
||||
|
||||
const instVal = document.getElementById('institution').value.trim();
|
||||
const institutions = instVal ? [instVal] : ["Independent"];
|
||||
@@ -595,7 +546,7 @@
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${tokenToUse}`
|
||||
'Authorization': `Bearer ${session.access_token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
display_name: document.getElementById('displayName').value,
|
||||
@@ -612,6 +563,8 @@
|
||||
});
|
||||
|
||||
if(!res.ok) throw new Error("Save failed");
|
||||
|
||||
// Success! Guard will now see profile as complete and allow dashboard access
|
||||
window.location.href = getBasePath() + '/dashboard.html';
|
||||
|
||||
} catch(err) {
|
||||
|
||||
77
tinytorch/site/extra/community/tests/e2e/auth.spec.js
Normal file
77
tinytorch/site/extra/community/tests/e2e/auth.spec.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Authentication Flow', () => {
|
||||
|
||||
test('Landing page should be accessible without login', async ({ page }) => {
|
||||
await page.goto('index.html');
|
||||
await expect(page).toHaveTitle(/AI History Landscape|The Tiny Torch/);
|
||||
// Check if login button is visible
|
||||
const authBtn = page.locator('#authBtn');
|
||||
await expect(authBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('Protected page (dashboard) should redirect to index with login action', async ({ page }) => {
|
||||
await page.goto('dashboard.html');
|
||||
// Guard should redirect to index.html?action=login&next=...
|
||||
await expect(page).toHaveURL(/index\.html\?action=login/);
|
||||
|
||||
// Auth modal should be active
|
||||
const authOverlay = page.locator('#authOverlay');
|
||||
await expect(authOverlay).toHaveClass(/active/);
|
||||
});
|
||||
|
||||
test('Manual Email Login - UI Interaction', async ({ page }) => {
|
||||
await page.goto('index.html?action=login');
|
||||
|
||||
// Switch to login mode if it defaults to signup
|
||||
const toggle = page.locator('#authToggle');
|
||||
const title = page.locator('#authTitle');
|
||||
|
||||
// Wait for modal to be ready
|
||||
await expect(page.locator('#authOverlay')).toHaveClass(/active/);
|
||||
|
||||
if ((await title.innerText()) === 'Create Account') {
|
||||
await toggle.click();
|
||||
}
|
||||
|
||||
await expect(title).toHaveText('Login');
|
||||
|
||||
// Fill in credentials (using dummy ones for UI test)
|
||||
await page.fill('#authEmail', 'test@example.com');
|
||||
await page.fill('#authPassword', 'password123');
|
||||
|
||||
// Click login
|
||||
const loginBtn = page.locator('#authSubmit');
|
||||
await expect(loginBtn).toHaveText('Login');
|
||||
});
|
||||
|
||||
test('Logout should clear session and redirect to index', async ({ page }) => {
|
||||
// Manually set a mock session
|
||||
await page.goto('index.html');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('tinytorch_token', 'mock-token');
|
||||
localStorage.setItem('tinytorch_user', JSON.stringify({ email: 'test@example.com' }));
|
||||
});
|
||||
|
||||
await page.reload();
|
||||
|
||||
// Open profile/logout modal
|
||||
await page.click('#authBtn');
|
||||
|
||||
// Listen for dialog (confirm logout)
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
|
||||
const logoutBtn = page.locator('#profileLogoutBtn');
|
||||
await expect(logoutBtn).toBeVisible();
|
||||
|
||||
// Wait for navigation after clicking logout
|
||||
await Promise.all([
|
||||
page.waitForURL(/index\.html/),
|
||||
logoutBtn.click()
|
||||
]);
|
||||
|
||||
// Verify session is completely purged
|
||||
const token = await page.evaluate(() => localStorage.getItem('tinytorch_token'));
|
||||
expect(token).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"testUser": {
|
||||
"email": "user@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
}
|
||||
48
tinytorch/site/extra/community/tests/e2e/lifecycle.spec.js
Normal file
48
tinytorch/site/extra/community/tests/e2e/lifecycle.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { testUser } = require('./credentials.json');
|
||||
|
||||
test.describe('User Account Lifecycle', () => {
|
||||
|
||||
test('Login and Navigate Profile', async ({ page }) => {
|
||||
// 1. Navigate to login
|
||||
await page.goto('index.html?action=login');
|
||||
|
||||
// Ensure we are in login mode
|
||||
const title = page.locator('#authTitle');
|
||||
if ((await title.innerText()) === 'Create Account') {
|
||||
await page.click('#authToggle');
|
||||
}
|
||||
|
||||
// 2. Perform Login
|
||||
await page.fill('#authEmail', testUser.email);
|
||||
await page.fill('#authPassword', testUser.password);
|
||||
|
||||
// We expect a redirect after login (to dashboard or profile_setup)
|
||||
await Promise.all([
|
||||
page.waitForURL(/dashboard\.html|profile_setup\.html/),
|
||||
page.click('#authSubmit')
|
||||
]);
|
||||
|
||||
console.log('✅ Logged in successfully');
|
||||
|
||||
// 3. Verify we can open the profile modal
|
||||
// Note: If redirect went to profile_setup.html, authBtn might already be active
|
||||
await page.click('#authBtn');
|
||||
|
||||
const profileOverlay = page.locator('#profileOverlay');
|
||||
await expect(profileOverlay).toHaveClass(/active/);
|
||||
|
||||
// 4. Check if the display name is loaded correctly
|
||||
const displayNameInput = page.locator('#profileDisplayName');
|
||||
await expect(displayNameInput).not.toHaveValue('');
|
||||
|
||||
console.log('✅ Profile data loaded correctly');
|
||||
|
||||
// 5. Navigate to Dashboard
|
||||
await page.goto('dashboard.html');
|
||||
await expect(page).toHaveURL(/dashboard\.html/);
|
||||
|
||||
console.log('✅ Navigation verified');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -967,11 +967,11 @@ Data Samples × Projection Matrix = Projected Data
|
||||
```
|
||||
Matrix Multiplication Process:
|
||||
A (2×3) B (3×2) C (2×2)
|
||||
┌ ┐ ┌ ┐ ┌ ┐
|
||||
│ 1 2 3 │ │ 7 8 │ │ 1×7+2×9+3×1 │ ┌ ┐
|
||||
│ │ × │ 9 1 │ = │ │ = │ 28 16 │
|
||||
│ 4 5 6 │ │ 1 2 │ │ 4×7+5×9+6×1 │ │ 79 49 │
|
||||
└ ┘ └ ┘ └ ┘ └ ┘
|
||||
┌ ┐ ┌ ┐ ┌ ┐
|
||||
│ 1 2 3 │ │ 7 8 │ │ 1×7+2×9+3×1 1*8+2*1+3*2 │ ┌ ┐
|
||||
│ │ × │ 9 1 │ = │ │ = │ 28 16 │
|
||||
│ 4 5 6 │ │ 1 2 │ │ 4×7+5×9+6×1 4*8+1*5+6*2 │ │ 79 49 │
|
||||
└ ┘ └ ┘ └ ┘ └ ┘
|
||||
|
||||
Computation Breakdown:
|
||||
C[0,0] = A[0,:] · B[:,0] = [1,2,3] · [7,9,1] = 1×7 + 2×9 + 3×1 = 28
|
||||
|
||||
Reference in New Issue
Block a user