diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b33ec3a..16bdd0f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,8 +13,8 @@ jobs: uses: actions/setup-node@v6 with: node-version: 20.x - cache: 'npm' - cache-dependency-path: '**/package-lock.json' + cache: "npm" + cache-dependency-path: "**/package-lock.json" - name: Install dependencies run: npm install --legacy-peer-deps diff --git a/package-lock.json b/package-lock.json index 462bb70..6745c0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@sanity/client": "^7.12.1", "@sanity/image-url": "^1.0.2", "@sanity/vision": "^3.25.0", "@splinetool/react-spline": "^2.2.6", @@ -33,6 +34,7 @@ "@vercel/speed-insights": "^1.0.10", "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", + "dotenv": "^17.2.3", "embla-carousel-autoplay": "^8.0.0-rc19", "embla-carousel-react": "^8.0.0-rc19", "framer-motion": "^10.18.0", @@ -4459,17 +4461,18 @@ } }, "node_modules/@sanity/client": { - "version": "6.15.11", - "resolved": "https://registry.npmjs.org/@sanity/client/-/client-6.15.11.tgz", - "integrity": "sha512-+dAEEKy6LrugjbssVnmef1ZoXOodiEj+0jMzqpygHmw0+uuyeb0jCvAnDdguF1thY/n3H1x+N1edt0CvVrD7Qg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-7.12.1.tgz", + "integrity": "sha512-AG4vW21+myuoQAWETF+zRtzeokruJfparVUT18DHb81lzabyb+d90+Be5Bo44P8leWDACPrJqmw9ES2KuotWnw==", + "license": "MIT", "dependencies": { - "@sanity/eventsource": "^5.0.0", - "@vercel/stega": "0.1.0", - "get-it": "^8.4.18", + "@sanity/eventsource": "^5.0.2", + "get-it": "^8.6.9", + "nanoid": "^3.3.11", "rxjs": "^7.0.0" }, "engines": { - "node": ">=14.18" + "node": ">=20" } }, "node_modules/@sanity/codegen": { @@ -4525,12 +4528,13 @@ } }, "node_modules/@sanity/eventsource": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sanity/eventsource/-/eventsource-5.0.1.tgz", - "integrity": "sha512-BFdRPTqVI76Nh18teu8850lV8DETdtJilFAlmQq/BdoXo88BSWBSTkIIi+H6AW1O9Nd7uT+9VRBqKuL2HKrYlA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sanity/eventsource/-/eventsource-5.0.2.tgz", + "integrity": "sha512-/B9PMkUvAlUrpRq0y+NzXgRv5lYCLxZNsBJD2WXVnqZYOfByL9oQBV7KiTaARuObp5hcQYuPfOAVjgXe3hrixA==", + "license": "MIT", "dependencies": { - "@types/event-source-polyfill": "1.0.2", - "@types/eventsource": "1.1.12", + "@types/event-source-polyfill": "1.0.5", + "@types/eventsource": "1.1.15", "event-source-polyfill": "1.0.31", "eventsource": "2.0.2" } @@ -4645,6 +4649,20 @@ "node": ">=18" } }, + "node_modules/@sanity/migrate/node_modules/@sanity/client": { + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-6.29.1.tgz", + "integrity": "sha512-BQRCMeDlBxwnMbFtB61HUxFf9aSb4HNVrpfrC7IFVqFf4cwcc3o5H8/nlrL9U3cDFedbe4W0AXt1mQzwbY/ljw==", + "license": "MIT", + "dependencies": { + "@sanity/eventsource": "^5.0.2", + "get-it": "^8.6.7", + "rxjs": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sanity/migrate/node_modules/arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -4842,6 +4860,20 @@ "@types/react": "^18.0.25" } }, + "node_modules/@sanity/types/node_modules/@sanity/client": { + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-6.29.1.tgz", + "integrity": "sha512-BQRCMeDlBxwnMbFtB61HUxFf9aSb4HNVrpfrC7IFVqFf4cwcc3o5H8/nlrL9U3cDFedbe4W0AXt1mQzwbY/ljw==", + "license": "MIT", + "dependencies": { + "@sanity/eventsource": "^5.0.2", + "get-it": "^8.6.7", + "rxjs": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sanity/ui": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@sanity/ui/-/ui-2.1.2.tgz", @@ -4902,6 +4934,20 @@ "node": ">=18" } }, + "node_modules/@sanity/util/node_modules/@sanity/client": { + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-6.29.1.tgz", + "integrity": "sha512-BQRCMeDlBxwnMbFtB61HUxFf9aSb4HNVrpfrC7IFVqFf4cwcc3o5H8/nlrL9U3cDFedbe4W0AXt1mQzwbY/ljw==", + "license": "MIT", + "dependencies": { + "@sanity/eventsource": "^5.0.2", + "get-it": "^8.6.7", + "rxjs": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sanity/uuid": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@sanity/uuid/-/uuid-3.0.2.tgz", @@ -5497,14 +5543,25 @@ "dev": true }, "node_modules/@types/event-source-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.2.tgz", - "integrity": "sha512-qE5zrFd73BRs5oSjVys6g/5GboqOMbzLRTUFPAhfULvvvbRAOXw9m4Wk+p1BtoZm4JgW7TljGGfVabBqvi3eig==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz", + "integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==", + "license": "MIT" }, "node_modules/@types/eventsource": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.12.tgz", - "integrity": "sha512-KlVguyxdoO8VkAhOMwOemK+NhFAg0gOwJHgimrWJUgM6LrdVW2nLa+d47WVWQcs8feRn0eeP+5yUDmDfzLBjRA==" + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", + "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==", + "license": "MIT" + }, + "node_modules/@types/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/glob": { "version": "7.2.0", @@ -8255,6 +8312,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", @@ -9127,15 +9196,16 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -9325,25 +9395,45 @@ } }, "node_modules/get-it": { - "version": "8.4.19", - "resolved": "https://registry.npmjs.org/get-it/-/get-it-8.4.19.tgz", - "integrity": "sha512-8OQeTNDfRJXQ6RIslyJukFhFBaLH2mdyS4puhX3q435Xx8z/zuv8XX2L7k5/nNWp95Hyoa9kyVs6gYKZiU2M1w==", + "version": "8.6.10", + "resolved": "https://registry.npmjs.org/get-it/-/get-it-8.6.10.tgz", + "integrity": "sha512-27StIK860ZVp2bhsG/aTWpcoA4OrFxtMqBbesa5sR23m5OxfVQYCnpm2rPQeo3gs5qsUk0FdkISLgXRJ4HynNw==", + "license": "MIT", "dependencies": { - "debug": "^4.3.4", + "@types/follow-redirects": "^1.14.4", "decompress-response": "^7.0.0", - "follow-redirects": "^1.15.6", - "into-stream": "^6.0.0", - "is-plain-object": "^5.0.0", + "follow-redirects": "^1.15.9", "is-retry-allowed": "^2.2.0", - "is-stream": "^2.0.1", - "parse-headers": "^2.0.5", - "progress-stream": "^2.0.0", + "through2": "^4.0.2", "tunnel-agent": "^0.6.0" }, "engines": { "node": ">=14.0.0" } }, + "node_modules/get-it/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/get-it/node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -9894,21 +9984,6 @@ "node": ">=10.13.0" } }, - "node_modules/into-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", - "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", - "dependencies": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -11323,15 +11398,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -11427,6 +11503,20 @@ "styled-components": "^5.2 || ^6.0" } }, + "node_modules/next-sanity/node_modules/@sanity/client": { + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-6.29.1.tgz", + "integrity": "sha512-BQRCMeDlBxwnMbFtB61HUxFf9aSb4HNVrpfrC7IFVqFf4cwcc3o5H8/nlrL9U3cDFedbe4W0AXt1mQzwbY/ljw==", + "license": "MIT", + "dependencies": { + "@sanity/eventsource": "^5.0.2", + "get-it": "^8.6.7", + "rxjs": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/next-themes": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", @@ -11981,14 +12071,6 @@ "node": ">=8" } }, - "node_modules/p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -12099,11 +12181,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/parse-headers": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", - "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==" - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -12806,15 +12883,6 @@ "node": ">=0.4.0" } }, - "node_modules/progress-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz", - "integrity": "sha512-xJwOWR46jcXUq6EH9yYyqp+I52skPySOeHfkxOZ2IY1AiBi/sFJhbhAKHoV3OTw/omQ45KTio9215dRJ2Yxd3Q==", - "dependencies": { - "speedometer": "~1.0.0", - "through2": "~2.0.3" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -13888,6 +13956,20 @@ "node": ">=18" } }, + "node_modules/sanity/node_modules/@sanity/client": { + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-6.29.1.tgz", + "integrity": "sha512-BQRCMeDlBxwnMbFtB61HUxFf9aSb4HNVrpfrC7IFVqFf4cwcc3o5H8/nlrL9U3cDFedbe4W0AXt1mQzwbY/ljw==", + "license": "MIT", + "dependencies": { + "@sanity/eventsource": "^5.0.2", + "get-it": "^8.6.7", + "rxjs": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/sanity/node_modules/framer-motion": { "version": "11.0.8", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.8.tgz", @@ -14391,11 +14473,6 @@ "node": ">=0.10.0" } }, - "node_modules/speedometer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz", - "integrity": "sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==" - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", diff --git a/package.json b/package.json index be550ae..aec1788 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@sanity/client": "^7.12.1", "@sanity/image-url": "^1.0.2", "@sanity/vision": "^3.25.0", "@splinetool/react-spline": "^2.2.6", @@ -88,6 +89,7 @@ "@vercel/speed-insights": "^1.0.10", "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", + "dotenv": "^17.2.3", "embla-carousel-autoplay": "^8.0.0-rc19", "embla-carousel-react": "^8.0.0-rc19", "framer-motion": "^10.18.0", diff --git a/public/sponsors/FracktalWorks.png b/public/sponsors/FracktalWorks.png new file mode 100644 index 0000000..b27b394 Binary files /dev/null and b/public/sponsors/FracktalWorks.png differ diff --git a/public/sponsors/OnlyScrews.png b/public/sponsors/OnlyScrews.png new file mode 100644 index 0000000..faa81d2 Binary files /dev/null and b/public/sponsors/OnlyScrews.png differ diff --git a/scripts/migrate-team-year.js b/scripts/migrate-team-year.js new file mode 100644 index 0000000..d9b0b64 --- /dev/null +++ b/scripts/migrate-team-year.js @@ -0,0 +1,82 @@ +const { createClient } = require("@sanity/client"); +require("dotenv").config({ path: ".env.local" }); + +const SANITY_PROJECT_ID = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID; +const SANITY_DATASET = process.env.NEXT_PUBLIC_SANITY_DATASET || "production"; +const SANITY_TOKEN = process.env.NEXT_PUBLIC_SANITY_TOKEN; +const DRY_RUN = process.env.DRY_RUN !== "false"; + +if (!SANITY_PROJECT_ID) { + console.error("SANITY_PROJECT_ID not set in .env.local"); + process.exit(1); +} + +const client = createClient({ + projectId: SANITY_PROJECT_ID, + dataset: SANITY_DATASET, + apiVersion: "2024-01-01", + token: SANITY_TOKEN, + useCdn: false, +}); + +async function run() { + console.log("Sanity teamMember.year migration"); + console.log("DRY_RUN:", DRY_RUN); + + const docs = await client.fetch(`*[_type == "teamMember" && defined(year)]{_id, _rev, year}`); + console.log("Found", docs.length, "documents with year field"); + + for (const doc of docs) { + const raw = doc.year; + if (!raw) continue; + + if (typeof raw === "string" && /^\d{4}$/.test(raw)) { + console.log(doc._id, "already year string:", raw); + continue; + } + + let yearString = null; + + if (typeof raw === "string") { + const m = raw.match(/^(\d{4})/); + if (m) yearString = m[1]; + } + + if (!yearString) { + try { + const d = new Date(raw); + if (!isNaN(d.getTime())) yearString = String(d.getUTCFullYear()); + } catch (e) { + // ignore + } + } + + if (!yearString) { + console.log(doc._id, "could not determine year from:", raw); + continue; + } + + console.log(doc._id, "->", yearString, DRY_RUN ? "(dry run)" : "(apply)"); + + if (!DRY_RUN) { + if (!SANITY_TOKEN) { + console.error("SANITY_TOKEN required to apply changes. Set it in .env.local"); + process.exit(1); + } + + try { + await client.patch(doc._id).set({ year: yearString }).commit({ autoGenerateArrayKeys: true }); + console.log("Patched", doc._id); + } catch (err) { + console.error("Failed to patch", doc._id, err.message || err); + } + } + } + + console.log("Migration finished"); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/app/robocup/page.tsx b/src/app/robocup/page.tsx index 6e87bd7..22eec2e 100644 --- a/src/app/robocup/page.tsx +++ b/src/app/robocup/page.tsx @@ -39,9 +39,9 @@ export default function Page(): React.ReactElement {
KARMA Robot diff --git a/src/app/team/alumni/page.tsx b/src/app/team/alumni/page.tsx new file mode 100644 index 0000000..4752ac4 --- /dev/null +++ b/src/app/team/alumni/page.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import Footer from "@/components/Footer"; +import Navbar from "@/components/Navbar"; +import AlumniTeamData from "@/components/Team/AlumniTeamData"; + +export default function AlumniTeamPage(): React.ReactElement { + return ( + <> + +
+ +
+
+ ); } diff --git a/src/components/Gallery/GalleryView.tsx b/src/components/Gallery/GalleryView.tsx index b3c4b4c..bb92b41 100644 --- a/src/components/Gallery/GalleryView.tsx +++ b/src/components/Gallery/GalleryView.tsx @@ -1,9 +1,10 @@ "use client"; import { motion } from "framer-motion"; -import React from "react"; +import React, { useState } from "react"; import Masonry, { ResponsiveMasonry } from "react-responsive-masonry"; +import { Lightbox } from "@/components/ui/lightbox"; import useGallery from "@/lib/hooks/useGallery"; import { fadeIn, textVariant } from "@/lib/motion"; @@ -12,6 +13,22 @@ import GalleryImage from "./GalleryImage"; export function GalleryView(): React.JSX.Element { const gallery = useGallery(); + const [lightboxOpen, setLightboxOpen] = useState(false); + const [currentIndex, setCurrentIndex] = useState(0); + + const handleImageClick = (index: number): void => { + setCurrentIndex(index); + setLightboxOpen(true); + }; + + const handleNext = (): void => { + setCurrentIndex((prev) => (prev + 1 < gallery.length ? prev + 1 : prev)); + }; + + const handlePrev = (): void => { + setCurrentIndex((prev) => (prev - 1 >= 0 ? prev - 1 : prev)); + }; + return (
{gallery.map((item, idx) => ( - +
handleImageClick(idx)} + role="button" + tabIndex={0} + onKeyDown={(e): void => { + if (e.key === "Enter") { + handleImageClick(idx); + } + }}> + +
))}
)}
+ + setLightboxOpen(false)} + onNext={handleNext} + onPrev={handlePrev} + />
); } diff --git a/src/components/Home/About.tsx b/src/components/Home/About.tsx index 0c16626..7d532b2 100644 --- a/src/components/Home/About.tsx +++ b/src/components/Home/About.tsx @@ -28,13 +28,14 @@ export function About(): React.JSX.Element { whileInView="show" viewport={{ once: true }} className="max-w-[700px] text-justify tracking-tight text-gray-600 dark:text-gray-400 md:text-lg/relaxed lg:text-base/relaxed xl:text-lg/relaxed"> - Team RoboManipal is the official Robotics team of MAHE, Manipal established in 2009. We are + Team RoboManipal is the official Robotics team of MAHE, Manipal established in 2010. We are a multidisciplinary team of 40+ undergraduate robotics enthusiasts who endeavour to achieve innovation through robotic technologies and spread knowledge about its diverse applications. - The legacy of 13 years to have won laurels at many levels is a testament to our enthusiasm. - We participate in prestigious competitions like ABU ROBOCON - Asia’s biggest Robotic - Competition. Robotreks and Robowars, the most exciting events of TechTatva which attract - teams from all over India, are organised by RoboManipal. + The legacy of 15 years to have won laurels at many levels is a testament to our enthusiasm. + We participate in prestigious competitions such as RoboCup - the world's largest robotics + competition - Technoxian, and ABU ROBOCON - Asia's biggest robotics competition. We also + conduct robotics research across diverse domains, developing innovative projects and + publishing papers. diff --git a/src/components/Home/Faq.tsx b/src/components/Home/Faq.tsx index d0b08ba..91f6be8 100644 --- a/src/components/Home/Faq.tsx +++ b/src/components/Home/Faq.tsx @@ -65,11 +65,15 @@ export function FAQ(): React.JSX.Element { What are the recent achievements of the team ? - 1. We received All India rank 21 in ABU ROBOCON
- 2. 2nd runner up in Technoxian World Cup
- 3. Qualified for all the nationals of ABU ROBOCON
- 4. AIR 9 in ABU ROBOCON in 2016
- 5. AIR 2 in the World Robotics Olympiad in 2018 + 1. AIR 1 in Ather Byte Battles 2.0 in 2025
+ 2. Second Runner-Up at Robonautica, IISc Rhapsody 3.0 in 2025
+ 3. Runners up in Tech Solstice 2025 at MIT-Bengaluru
+ 4. 1st place at Tech-TED Project Presentation in 2025
+ 5. Second runners up in Technoxian World Cup 2024
+ 6. Group Leaders in ABU ROBOCON Nationals 2024
+ 7. Received All India rank 21 in ABU ROBOCON'23
+ 8. AIR 2 in the World Robotics Olympiad in 2018
+ 9. AIR 9 in ABU ROBOCON in 2016
@@ -77,17 +81,24 @@ export function FAQ(): React.JSX.Element { What are the research projects that team is curently working on? - 1. ARNAV: A biomimetic hand project.
- 2. Crab bot: A semi-autonomous robot designed for social purposes, utilizing swarm robotics - mechanisms.
- 3. PRAYAS: A 6-DOF serial manipulator project.
- 4. PEEKER: A surveillance robot equipped with a 3-DOF camera.
- 5. Swerve drive: An omnidirectional drive-train in which all wheels are independently - steered and driven.
- 6. RMMD (RoboManipal Motor Driver): An in-house-built motor driver used to efficiently - control motor speed and direction, featuring built-in reverse current protection.
- 7. Tachometer: A fully in-house-built tachometer, including a microcontrolling unit, printed - circuit board, and LED screen.
+ 1. KARMA: A 6-DOF robotic arm mounted on a mobile base, capable of autonomous navigation and + performing tasks such as object detection, picking, and placing. Built for real-world task + execution using advanced motion planning and intelligent control.
+ 2. Angulator: A compact, real-time orientation tracking device that provides precise yaw, + pitch, and roll data using an nRF module with ESB wireless communication. Designed for + multi-robot motion analysis with ultra-low latency and easy mounting.
+ 3. Swerve Drive: An omnidirectional drive-train in which all wheels are independently + steered and driven, enabling highly agile and precise movement for competition robots.{" "} +
+ 4. RMMD V2 (RoboManipal Motor Driver): Developed the second iteration of an in-house motor + driver, fully integrated as a standalone ROS node for direct robot communication. Optimized + the power stage to significantly reduce power consumption compared to the validated V1 + prototype, while retaining reverse-current protection.
+ 5. Tachometer: A fully in-house-built tachometer featuring a microcontroller unit, custom + PCB, and LED display for accurate RPM measurement and system monitoring.
+ 6. FarmBot: It is an autonomous field robot that combines computer vision, soil sensing, and + AI-based disease detection to give Indian farmers real-time, actionable insights for + reducing crop loss and promoting sustainable farming.
diff --git a/src/components/Home/Sponsors.tsx b/src/components/Home/Sponsors.tsx index 3ff4c86..3c7e872 100644 --- a/src/components/Home/Sponsors.tsx +++ b/src/components/Home/Sponsors.tsx @@ -4,7 +4,8 @@ import Autoplay from "embla-carousel-autoplay"; import { motion } from "framer-motion"; import Image from "next/image"; import Link from "next/link"; -import React, { useRef } from "react"; +import { useTheme } from "next-themes"; +import React, { useEffect, useRef, useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel"; @@ -16,6 +17,14 @@ import SectionWrapper from "../wrappers/SectionWrapper"; export function Sponsors(): React.JSX.Element { const sponsors = useSponsors(); const player = useRef(Autoplay({ delay: 2000, stopOnInteraction: true })); + const { resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + const LIGHT_OVERRIDES: Record = { + OnlyScrews: "/sponsors/OnlyScrews.png", + "Fracktal Works": "/sponsors/FracktalWorks.png", + }; return ( <> {sponsor.name} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 46f913a..195b573 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,5 +1,6 @@ "use client"; +import { ChevronDown, Info, Users } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -21,6 +22,7 @@ import { navLinks } from "@/lib/constants"; import { cn } from "@/lib/utils"; import ThemeToggleButton from "./ThemeToggle"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"; import { buttonVariants } from "./ui/button"; export default function Navbar(): React.JSX.Element { @@ -28,6 +30,7 @@ export default function Navbar(): React.JSX.Element { const [logo, setLogo] = React.useState("/logo_light.png"); const { resolvedTheme } = useTheme(); const navBarRef = useRef(null); + const [sheetOpen, setSheetOpen] = React.useState(false); React.useEffect(() => { const handleScroll = (): void => { if (navBarRef.current) { @@ -65,15 +68,22 @@ export default function Navbar(): React.JSX.Element { - About Us + About Us - {navLinks.map((link) => ( - - - {link.label} - - - ))} + {navLinks.map((link) => { + if (link.label === "Team") { + return ( + + ); + } + return ( + + + {link.label} + + + ); + })}
- + - - + + Menu -
- {navLinks.map((link) => ( - - - {link.label} - - ))} +
+ + + svg]:hidden" + )}> + + + About Us + + + +
+ setSheetOpen(false)}> + About Us + + setSheetOpen(false)}> + Sponsors + + setSheetOpen(false)}> + Contact + +
+
+
+
+ + {navLinks.map((link): React.ReactNode => { + if (link.label === "Team") { + return ( + + + svg]:hidden" + )}> + + + Team + + + +
+ setSheetOpen(false)}> + Current Team + + setSheetOpen(false)}> + Alumni + +
+
+
+
+ ); + } + return ( + setSheetOpen(false)}> + + {link.label} + + ); + })} + +
diff --git a/src/components/Team/AlumniDatePicker.tsx b/src/components/Team/AlumniDatePicker.tsx new file mode 100644 index 0000000..66937d6 --- /dev/null +++ b/src/components/Team/AlumniDatePicker.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { format } from "date-fns"; +import { Calendar as CalendarIcon, Check } from "lucide-react"; +import { useTheme } from "next-themes"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +import { ShineBorder } from "../ui/shine-border"; + +export function AlumniDatePicker({ + date, + setDate, + availableYears = [], +}: { + date: Date | undefined; + setDate: React.Dispatch>; + availableYears?: number[]; + minDate?: Date | undefined; + maxDate?: Date | undefined; +}): React.ReactElement { + const { resolvedTheme } = useTheme(); + const selectedYear = date?.getFullYear(); + + function handleSelect(year: number): void { + const newDate = new Date(date ?? new Date()); + newDate.setFullYear(year); + setDate(newDate); + } + + return ( + + + + + +
+
    + {availableYears.map((y): React.ReactElement => { + const isActive = y === selectedYear; + return ( +
  • + +
  • + ); + })} +
+
+
+
+ ); +} diff --git a/src/components/Team/AlumniTeamData.tsx b/src/components/Team/AlumniTeamData.tsx new file mode 100644 index 0000000..61a586a --- /dev/null +++ b/src/components/Team/AlumniTeamData.tsx @@ -0,0 +1,106 @@ +"use client"; + +import React, { useEffect, useMemo } from "react"; + +import TeamCategory from "@/components/Team/TeamCategory"; +import useMembers from "@/lib/hooks/useMembers"; + +import { AlumniDatePicker } from "./AlumniDatePicker"; + +function AlumniTeamData(): React.ReactElement { + const members = useMembers(); + const [date, setDate] = React.useState(undefined); + const [displayYear, setDisplayYear] = React.useState(undefined); + const [alumniYears, setAlumniYears] = React.useState([]); + const [minDate, maxDate] = useMemo<[Date | undefined, Date | undefined]>(() => { + if (Object.keys(members).length === 0) return [undefined, undefined]; + + const currentYear = new Date().getFullYear(); + const alumniMembers = Object.entries(members).filter(([year]) => parseInt(year) <= currentYear - 1); + + if (alumniMembers.length === 0) return [undefined, undefined]; + + const dates = alumniMembers.flatMap(([, yearMembers]) => yearMembers.map((member) => member.year)); + const minDate = dates.reduce((min, date) => (date < min ? date : min), dates[0]); + const maxDate = dates.reduce((max, date) => (date > max ? date : max), dates[0]); + return [minDate, maxDate]; + }, [members]); + + useEffect(() => { + const currentYear = new Date().getFullYear(); + const collected = Object.keys(members) + .map((year) => parseInt(year)) + .filter((year) => year <= currentYear - 1) + .sort((a, b) => b - a); + setAlumniYears(collected); + }, [members]); + + useEffect(() => { + if (!Array.isArray(alumniYears) || alumniYears.length === 0) return; + if (typeof window === "undefined") return; + const nav = + typeof window.performance.getEntriesByType === "function" + ? window.performance.getEntriesByType("navigation") + : []; + const isReload = + Array.isArray(nav) && nav.length > 0 && (nav[0] as PerformanceNavigationTiming).type === "reload"; + if (!isReload) return; + const saved = window.localStorage.getItem("alumniYear"); + if (!saved) return; + const y = parseInt(saved); + if (Number.isNaN(y)) return; + if (!alumniYears.includes(y)) return; + const d = new Date(); + d.setFullYear(y); + setDate(d); + setDisplayYear(y); + }, [alumniYears]); + + useEffect(() => { + if (!maxDate || date) return; + setDate(maxDate); + setDisplayYear(maxDate.getFullYear()); + }, [maxDate, date]); + + useEffect(() => { + if (!date) return; + const y = date.getFullYear(); + const yearMembers = members[String(y)]; + if (Array.isArray(yearMembers) && y <= new Date().getFullYear() - 1) { + setDisplayYear(y); + if (typeof window !== "undefined") { + window.localStorage.setItem("alumniYear", String(y)); + } + return; + } + if (!Array.isArray(alumniYears) || alumniYears.length === 0) return; + const sorted = [...alumniYears].sort((a, b) => a - b); + let nearest: number | undefined = undefined; + let bestDist = Number.POSITIVE_INFINITY; + for (const candidate of sorted) { + const dist = Math.abs(candidate - y); + if (dist < bestDist || (dist === bestDist && (nearest === undefined || candidate > nearest))) { + bestDist = dist; + nearest = candidate; + } + } + if (nearest !== undefined) setDisplayYear(nearest); + }, [date, members, alumniYears]); + + return ( +
+
+ {displayYear !== undefined && Array.isArray(members[String(displayYear)]) && ( + + )} +
+ {minDate && maxDate && ( +
+ +
+ )} +
+ ); +} + +export default AlumniTeamData; diff --git a/src/components/Team/CurrentTeamData.tsx b/src/components/Team/CurrentTeamData.tsx new file mode 100644 index 0000000..2a289cc --- /dev/null +++ b/src/components/Team/CurrentTeamData.tsx @@ -0,0 +1,28 @@ +"use client"; + +import React, { useEffect } from "react"; + +import TeamCategory from "@/components/Team/TeamCategory"; +import useMembers from "@/lib/hooks/useMembers"; +import { TeamMember } from "@/lib/models"; + +function CurrentTeamData(): React.ReactElement { + const members = useMembers(); + const [currentTeamMembers, setCurrentTeamMembers] = React.useState([]); + + useEffect(() => { + const currentYear = new Date().getFullYear(); + const yearMembers = members[String(currentYear)]; + setCurrentTeamMembers(Array.isArray(yearMembers) ? yearMembers : []); + }, [members]); + + return ( +
+
+ {currentTeamMembers.length > 0 && } +
+
+ ); +} + +export default CurrentTeamData; diff --git a/src/components/Team/TeamCategory.tsx b/src/components/Team/TeamCategory.tsx index 5a389e4..b6dca23 100644 --- a/src/components/Team/TeamCategory.tsx +++ b/src/components/Team/TeamCategory.tsx @@ -10,15 +10,25 @@ import { textVariant } from "@/lib/motion"; import TeamCard from "./TeamCard"; const SubSystemSection = ({ subsystem, details }: { subsystem: string; details: TeamMember[] }): React.JSX.Element => { - const members = useMemo( - () => - details.filter( - (member) => - member.subsystem === subsystem && - (member.role.toLowerCase() === "member" || member.role.toLowerCase() == "senior member") - ), - [details, subsystem] - ); + const members = useMemo(() => { + const filtered = details.filter((member) => { + const role = (member.role || "").toLowerCase(); + return ( + Boolean(member.subsystem) && + member.subsystem === subsystem && + (role === "member" || role === "senior member") + ); + }); + return filtered.sort((a, b) => { + const roleA = (a.role || "").toLowerCase(); + const roleB = (b.role || "").toLowerCase(); + if (roleA === "senior member" && roleB !== "senior member") return -1; + if (roleA !== "senior member" && roleB === "senior member") return 1; + return 0; + }); + }, [details, subsystem]); + const subKey = (subsystem || "").toLowerCase(); + const subText = subsystemText[subKey]; return ( <> {members.length === 0 ? null : ( @@ -32,16 +42,16 @@ const SubSystemSection = ({ subsystem, details }: { subsystem: string; details:

{subsystem}

- - "{subsystemText[subsystem.toLowerCase()].split("\n")[0]}" - -

- { - subsystemText[subsystem.toLowerCase()].split("\n")[ - subsystemText[subsystem.toLowerCase()].split("\n").length - 1 - ] - } -

+ {subText && ( + <> + + "{subText.split("\n")[0]}" + +

+ {subText.split("\n")[subText.split("\n").length - 1]} +

+ + )}
{members.map((member) => ( @@ -57,13 +67,24 @@ const SubSystemSection = ({ subsystem, details }: { subsystem: string; details: const HeadDetails = ({ details }: { details: TeamMember[] }): React.JSX.Element => { const managers = useMemo( () => - details.filter( - (member) => - member.role.toLowerCase().includes("team") || member.role.toLowerCase().includes("tech lead") - ), + details.filter((member) => { + const role = (member.role || "").toLowerCase(); + return role.includes("team") || role.includes("tech lead"); + }), [details] ); - const heads = useMemo(() => details.filter((member) => member.role.toLowerCase().includes("head")), [details]); + const heads = useMemo( + () => + details.filter((member) => { + const role = (member.role || "").toLowerCase(); + return role.includes("head") || role === "founder"; + }), + [details] + ); + + if (managers.length === 0 && heads.length === 0) { + return <>; + } return ( <> @@ -105,7 +126,7 @@ export default function TeamCategory({ details }: { details: TeamMember[] }): Re const subsystems = useMemo(() => { const subsystems = new Set(); details.forEach((member) => { - subsystems.add(member.subsystem); + if (member.subsystem) subsystems.add(member.subsystem); }); return Array.from(subsystems); }, [details]); diff --git a/src/components/Work/WorkCard.tsx b/src/components/Work/WorkCard.tsx index c5ae43d..0c192a1 100644 --- a/src/components/Work/WorkCard.tsx +++ b/src/components/Work/WorkCard.tsx @@ -10,113 +10,79 @@ import { LuCalendarClock, LuClock } from "react-icons/lu"; import { RiExternalLinkLine } from "react-icons/ri"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; import { Work } from "@/lib/models"; import { Badge } from "../ui/badge"; import { Separator } from "../ui/separator"; +import { WorkDetailModal } from "./WorkDetailModal"; export default function WorkCard({ work }: { work: Work }): React.JSX.Element { const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); return ( - - - - -
- {work.title} setLoading(false)} - /> -
-
- - - - {work.title} -
- {moment(work.date).fromNow()} - - {work.status === "Completed" ? ( - - ) : work.status === "Upcoming" ? ( - - ) : ( - - )} - {work.status} - -
-
- - {work.title} - - {work.description} - - -
-
-
-
- {work.title} - - - {moment(work.date).fromNow()} - -
-
- - {work.description} - - - -
- - {work.status === "Completed" ? ( - - ) : work.status === "Upcoming" ? ( - - ) : ( - + <> + + +
setModalOpen(true)} + role="button" + tabIndex={0} + onKeyDown={(e): void => { + if (e.key === "Enter") { + setModalOpen(true); + } + }}> + {work.title} setLoading(false)} + /> +
+
+ {work.title} + + + {moment(work.date).fromNow()} + +
+
+ + {work.description} + + + +
+ + {work.status === "Completed" ? ( + + ) : work.status === "Upcoming" ? ( + + ) : ( + + )} + {work.status} + + {work.link && ( + + + )} - {work.status} - - {work.link && ( - - - - )} -
-
-
+
+
+
+ + setModalOpen(false)} /> + ); } diff --git a/src/components/Work/WorkData.tsx b/src/components/Work/WorkData.tsx index 2a29ca5..db58a69 100644 --- a/src/components/Work/WorkData.tsx +++ b/src/components/Work/WorkData.tsx @@ -47,14 +47,11 @@ export function WorkData(): React.ReactElement {

+ className="my-16 flex flex-wrap items-center justify-center gap-16" + style={{ maxWidth: "calc(3 * 20rem + 2 * 4rem)" }}> {status[key].map((work) => ( ))} - {/* spacer to ensure last card fully visible on all viewports */} -
))} diff --git a/src/components/Work/WorkDetailModal.tsx b/src/components/Work/WorkDetailModal.tsx new file mode 100644 index 0000000..3fdf957 --- /dev/null +++ b/src/components/Work/WorkDetailModal.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { X } from "lucide-react"; +import moment from "moment"; +import Image from "next/image"; +import Link from "next/link"; +import React from "react"; +import { GoVerified } from "react-icons/go"; +import { LuCalendarClock, LuClock } from "react-icons/lu"; +import { RiExternalLinkLine } from "react-icons/ri"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Work } from "@/lib/models"; + +interface WorkDetailModalProps { + work: Work; + isOpen: boolean; + onClose: () => void; +} + +export function WorkDetailModal({ work, isOpen, onClose }: WorkDetailModalProps): React.ReactElement | null { + if (!isOpen) return null; + + return ( +
+ + +
e.stopPropagation()}> +
+

{work.title}

+
+ {moment(work.date).fromNow()} + + {work.status === "Completed" ? ( + + ) : work.status === "Upcoming" ? ( + + ) : ( + + )} + {work.status} + +
+
+ +
+ {work.title} +
+ +
+

Description

+

+ {work.description} +

+
+ + {work.link && ( + + + + )} +
+
+ ); +} diff --git a/src/components/ui/lightbox.tsx b/src/components/ui/lightbox.tsx new file mode 100644 index 0000000..b3c9d9b --- /dev/null +++ b/src/components/ui/lightbox.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { ChevronLeft, ChevronRight, X } from "lucide-react"; +import Image from "next/image"; +import React from "react"; + +import { GalleryItem } from "@/lib/models"; +import { cn } from "@/lib/utils"; + +import { Button } from "./button"; + +interface LightboxProps { + isOpen: boolean; + items: GalleryItem[]; + currentIndex: number; + onClose: () => void; + onNext: () => void; + onPrev: () => void; +} + +export function Lightbox({ + isOpen, + items, + currentIndex, + onClose, + onNext, + onPrev, +}: LightboxProps): React.ReactElement | null { + if (!isOpen || !items[currentIndex]) return null; + + const currentItem = items[currentIndex]; + + return ( +
+ +
e.stopPropagation()}> + {currentItem.title} +
+

{currentItem.title}

+

+ {currentIndex + 1} / {items.length} +

+
+ {items.length > 1 && ( + <> + + + + )} +
+ +
+ ); +} + +function LightboxKeyboard({ + onClose, + onNext, + onPrev, +}: { + onClose: () => void; + onNext: () => void; + onPrev: () => void; +}): null { + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent): void => { + if (e.key === "Escape") onClose(); + if (e.key === "ArrowRight") onNext(); + if (e.key === "ArrowLeft") onPrev(); + }; + + window.addEventListener("keydown", handleKeyDown); + return (): void => window.removeEventListener("keydown", handleKeyDown); + }, [onClose, onNext, onPrev]); + + return null; +} diff --git a/src/components/ui/scroll-to-top.tsx b/src/components/ui/scroll-to-top.tsx index 24bd763..51ebd39 100644 --- a/src/components/ui/scroll-to-top.tsx +++ b/src/components/ui/scroll-to-top.tsx @@ -59,7 +59,7 @@ export function ScrollToTop(): React.ReactElement { return (