Compare commits

..

3 commits

Author SHA1 Message Date
chark1es
ae2616c1eb centered and blended 404 page 2025-01-06 18:35:43 -08:00
chark1es
6e5fb423b1 fix image issues 2025-01-06 18:35:27 -08:00
chark1es
ae9691c78a fixed in-view 2025-01-06 17:56:17 -08:00
333 changed files with 10687 additions and 48012 deletions

View file

@ -1 +0,0 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)

3
.gitignore vendored
View file

@ -3,9 +3,6 @@ dist/
# generated types
.astro/
.cursor
final_review_gate.py
# dependencies
node_modules/

View file

@ -1,7 +0,0 @@
{
"workbench.colorCustomizations": {
"activityBar.background": "#221489",
"titleBar.activeBackground": "#301DC0",
"titleBar.activeForeground": "#F9F9FE"
}
}

View file

@ -1,55 +0,0 @@
# Use the official Bun image
FROM oven/bun:1.1
# Install dependencies for Puppeteer and Chrome/Chromium
RUN apt-get update && \
apt-get install -y \
wget \
ca-certificates \
fonts-liberation \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
libdbus-1-3 \
libdrm2 \
libgbm1 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
xdg-utils \
chromium \
gnupg \
--no-install-recommends && \
# Install Google Chrome stable
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
apt-get update && \
apt-get install -y google-chrome-stable && \
rm -rf /var/lib/apt/lists/*
# Set Puppeteer executable path (prefer google-chrome-stable, fallback to chromium)
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable
# Set working directory
WORKDIR /app
# Copy package files and install dependencies
COPY bun.lock package.json ./
RUN bun install
# Copy the rest of your app
COPY . .
# Build the application
RUN bun run build
# Expose the port your app runs on (change if needed)
EXPOSE 4321
# Start the server
CMD ["bun", "run", "start"]

View file

@ -3,14 +3,12 @@
Development version of the IEEE UC San Diego student branch website.
## Figma Design
- [IEEE UCSD Website Design](https://www.figma.com/design/AihoR936yUmYrMoCZJ0LF7/UCSD-IEEE?node-id=0-1&t=ajK9lKroQFJbokFS-1)
- [IEEE UCSD Website Design](https://www.figma.com/design/AihoR936yUmYrMoCZJ0LF7/UCSD-IEEE?node-id=0-1&t=ajK9lKroQFJbokFS-1)
## Getting Started
Prerequisites:
- [Bun](https://bun.sh) - Fast all-in-one JavaScript runtime & toolkit
- [Bun](https://bun.sh) - Fast all-in-one JavaScript runtime & toolkit
### Installation
@ -48,17 +46,9 @@ bun run start
## Built with:
- [Astro](https://astro.build) - Web framework for content-driven websites
- [React](https://react.dev) - UI components
- [TailwindCSS](https://tailwindcss.com) - Styling
- This manifests as one line CSS like `class="flex border-white/40 border-[0.1vw] rounded-[2vw] h-[85vh] px-[10%] py-[3%] bg-gradient-to-t to-ieee-blue-100/30 via-ieee-black from-ieee-black"`
- [TailwindAnimated](https://www.tailwindcss-animated.com/)
- This is what we use for all of our nice animations!
- [MDX](https://mdxjs.com) - Enhanced Markdown
- [Expressive Code](https://expressive-code.com) - Beautiful code blocks
- [Node.js Adapter](https://docs.astro.build/en/guides/integrations-guide/node/) - Server-side rendering
## Contributors:
- Charles Nguyen
- Shing Hung
- [Astro](https://astro.build) - Web framework for content-driven websites
- [React](https://react.dev) - UI components
- [TailwindCSS](https://tailwindcss.com) - Styling
- [MDX](https://mdxjs.com) - Enhanced Markdown
- [Expressive Code](https://expressive-code.com) - Beautiful code blocks
- [Node.js Adapter](https://docs.astro.build/en/guides/integrations-guide/node/) - Server-side rendering

View file

@ -15,29 +15,9 @@ import icon from "astro-icon";
// https://astro.build/config
export default defineConfig({
output: "server",
integrations: [tailwind(), expressiveCode(), react(), icon(), mdx()],
integrations: [tailwind(), expressiveCode(), react(), icon(), mdx()],
adapter: node({
mode: "standalone",
}),
// Define environment variables that should be available to client components
vite: {
define: {
"import.meta.env.LOGTO_APP_ID": JSON.stringify(process.env.LOGTO_APP_ID),
"import.meta.env.LOGTO_APP_SECRET": JSON.stringify(
process.env.LOGTO_APP_SECRET,
),
"import.meta.env.LOGTO_ENDPOINT": JSON.stringify(
process.env.LOGTO_ENDPOINT,
),
"import.meta.env.LOGTO_TOKEN_ENDPOINT": JSON.stringify(
process.env.LOGTO_TOKEN_ENDPOINT,
),
"import.meta.env.LOGTO_API_ENDPOINT": JSON.stringify(
process.env.LOGTO_API_ENDPOINT,
),
},
},
adapter: node({
mode: "standalone",
}),
});

2250
bun.lock

File diff suppressed because it is too large Load diff

BIN
bun.lockb Normal file

Binary file not shown.

View file

@ -1,12 +0,0 @@
version: "3.8"
services:
web:
build: .
ports:
- "4321:4321"
environment:
- NODE_ENV=production
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable
- PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
- PUPPETEER_DISABLE_SANDBOX=true

View file

@ -1,4 +0,0 @@
[phases.setup]
nixPkgs = ["nodejs_20", "bun"]
aptPkgs = ["curl", "wget"]

207
notes.md
View file

@ -1,207 +0,0 @@
# Event Request Form:
Prior Notes:
Whether you are or aren't requesting AS Funding or physical flyers, you MUST submit this request form at least 6 weeks before your event. We can create both digital and physical flyers for your event, advertise your event on social media (Facebook, Instagram, Discord), advertise your event on newsletters (IEEE, ECE, IDEA), take pictures at your event and edit them (we highly recommend this!), and livestream your event on Facebook. After submitting this form, please @-pr and/or @-coordinators in #-events on Slack.
Please note that if you submit your request late, we may deny your request.
Also note that if you're requesting AS Funding, please don't forget to check the Funding Guide or the Google Calendar for the funding request deadlines.
### Do you need graphics from our design team?
Possible Answers:
- Yes (Go to Section 2)
- No (Go to Section 3)
## Section 2: PR
If you need PR Materials, please don't forget that this form MUST be submitted at least 6 weeks in advance even if you aren't requesting AS funding or physical flyers. Also, please remember to ping PR in #-events on Slack once you've submitted this form.
### Type of material needed?
Feel free to add what else you need as well as where else you want your event advertised (if needed) in the other option.
- Digital flyer (with social media advertising: Facebook, Instagram, Discord)
- Digital flyer (with NO social media advertising)
- Physical flyer (with advertising)
- Physical flyer (with NO advertising)
- Newsletter (IEEE, ECE, IDEA)
- Other
### If you chose to have your flyer advertised, when do you need us to start advertising?
- DATETIME
### Logos Required?
[ ] IEEE
[ ] AS (required if funded by AS)
[ ] HKN
[ ] TESC
[ ] PIB
[ ] TNT
[ ] SWE
[ ] OTHER (please upload transparent logo files to the next question)
### Please share your logo files here:
FILEUPLOAD
### What format do you need it to be in?
- PNG
- PDF
- JPG
- DOES NOT MATTER
### Any other specifications and requests (color scheme, overall design, etc)? Feel free to link us to any examples you want us to consider in designing your promotional material. (i.e. past FB covers from events, etc)
TEXTBOX
### Photography Needed?
- Yes
- No
## Section 3: Event Details
Please remember to ping @Coordinators in #-events on Slack once you've submitted this form so that they can fill out a TAP form for you.
### Event Name:
TEXTBOX
### Event Description:
TEXTBOX
### Event Start Date:
DATETIME
### Event End Date:
DATETIME
### Event Location:
TEXTBOX
### Do you/will you have a room booking for this event?
- Yes
- No
## Section 4: TAP Form
Please ensure you have ALL sections completed, if something is not available, let the coordinators know and be advised on how to proceed.
### Expected attendance? Include a number NOT a range please.
(
PROGRAMMING FUNDS
EVENTS FUNDED BY PROGRAMMING FUNDS MAY ONLY ADMIT UC SAN DIEGO STUDENTS, STAFF OR FACULTY AS GUESTS.
ONLY UC SAN DIEGO UNDERGRADUATE STUDENTS MAY RECEIVE ITEMS FUNDED BY THE ASSOCIATED STUDENTS.
EVENT FUNDING IS GRANTED UP TO A MAXIMUM OF $10.00 PER EXPECTED STUDENT ATTENDEE AND $5,000 PER EVENT
)
NUMBER
### Upload your room booking here. Ensure your file size fits within this size. Please use the following naming format: EventName_LocationOfEvent_DateOfEvent
i.e. ArduinoWorkshop_Qualcomm_01/06/2025
FILEUPLOAD
### Do you need AS Funding? (food/flyers)
- Yes
- No
### Will you be serving food/drinks at your event?
- Yes
- No
## Section 5: AS Funding
Please make sure the restaurant is a valid AS Funding food vendor! An invoice can be an unofficial receipt. Just make sure that the restaurant name and location, desired pickup or delivery date and time, all the items ordered plus their prices, discount/fees/tax/tip, and total are on the invoice! We don't recommend paying out of pocket because reimbursements can be a hassle when you're not a Principal Member.
### Please put your invoice information in the following format: quantity + item name + unit cost + discounts/fees/tax/tip + total + vendor.
(e.g. 3-Chicken Cutlet with Gravy Regular, white rice, and mac salad x14.95 each | 3-Garlic Shrimp Regular with white rice and mac salad x15.45 each | 10-Spam Musubi x2.95 each | Tax = 9.35 | Tip = 18.10 | Total = 148.15 from L&L Hawaiian Barbeque)
TEXTBOX
### Be sure to share a screenshot of your order/your official food invoice here. Official food invoices will be required 2 weeks before the start of your event. Please use the following naming format: EventName_OrderLocation_DateOfEvent
i.e. QPWorkathon#1_PapaJohns_01/06/2025
FILEUPLOAD
Pocketbase Collection Schema:
```json
{
"collectionId": "pbc_1475615553",
"collectionName": "event_request",
"id": "test",
"requested_user": "RELATION_RECORD_ID",
"name": "test",
"location": "test",
"start_date_time": "2022-01-01 10:00:00.123Z",
"end_date_time": "2022-01-01 10:00:00.123Z",
"flyers_needed": true,
"flyer_type": [
"digital_with_social",
"digital_no_social",
"physical_with_advertising",
"physical_no_advertising",
"newsletter",
"other"
],
"other_flyer_type": "test",
"flyer_advertising_start_date": "test",
"flyer_additional_requests": "test",
"photography_needed": true,
"required_logos": ["IEEE", "AS", "HKN", "TESC", "PIB", "TNT", "SWE", "OTHER"],
"other_logos": ["filename.jpg"],
"advertising_format": "pdf",
"will_or_have_room_booking": true,
"expected_attendance": 123,
"room_booking": "filename.jpg",
"as_funding_required": true,
"food_drinks_being_served": true,
"itemized_invoice": "JSON",
"invoice": "filename.jpg",
"created": "2022-01-01 10:00:00.123Z",
"updated": "2022-01-01 10:00:00.123Z"
}
```
Possible Flyer Types:
- digital_with_social
- digital_no_social
- physical_with_advertising
- physical_no_advertising
- newsletter
- other
Possible Logos:
[ ] IEEE
[ ] AS
[ ] HKN
[ ] TESC
[ ] PIB
[ ] TNT
[ ] SWE
[ ] OTHER
Possible Advertising Formats:
- pdf
- jpeg
- png
- does_not_matter

9385
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,9 +2,6 @@
"name": "ieeeucsd-dev",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=18.20.8"
},
"scripts": {
"dev": "astro dev",
"start": "node ./dist/server/entry.mjs",
@ -13,49 +10,23 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.2.3",
"@astrojs/node": "^9.1.3",
"@astrojs/react": "^4.2.3",
"@astrojs/tailwind": "^6.0.2",
"@heroui/react": "^2.7.5",
"@iconify-json/heroicons": "^1.2.2",
"@iconify-json/mdi": "^1.2.3",
"@iconify/react": "^5.2.0",
"@types/highlight.js": "^10.1.0",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.15",
"@types/puppeteer": "^7.0.4",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.1",
"astro": "^5.5.6",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"chart.js": "^4.4.7",
"dexie": "^4.0.11",
"framer-motion": "^12.6.2",
"highlight.js": "^11.11.1",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"@astrojs/mdx": "4.0.3",
"@astrojs/node": "^9.0.0",
"@astrojs/react": "4.1.2",
"@astrojs/tailwind": "5.1.4",
"@types/react": "^18.3.14",
"@types/react-dom": "^18.3.2",
"astro": "5.1.1",
"astro-expressive-code": "^0.38.3",
"astro-icon": "^1.1.4",
"motion": "^11.15.0",
"next": "^15.1.2",
"pocketbase": "^0.25.1",
"prismjs": "^1.29.0",
"puppeteer": "^24.10.1",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0",
"react-hot-toast": "^2.5.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"rehype-expressive-code": "^0.40.2",
"resend": "^4.5.1",
"tailwindcss": "^3.4.16",
"typescript": "^5.8.3"
"tailwindcss": "^3.4.16"
},
"devDependencies": {
"@types/prismjs": "^1.26.5",
"daisyui": "^4.12.23",
"prettier": "^3.4.2",
"prettier-plugin-astro": "^0.14.1",
"tailwindcss-animated": "^1.1.2",

BIN
public/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

BIN
public/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

BIN
public/halloween.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

BIN
public/hardhack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

BIN
public/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
public/officers/akhil.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,020 KiB

BIN
public/officers/allie.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 987 KiB

BIN
public/officers/andy.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 KiB

BIN
public/officers/anika.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/officers/anu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/officers/ashlee.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/officers/charles.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 540 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/officers/dhruv.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/officers/dihan.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/officers/emma.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/officers/erik.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/officers/lauren.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/officers/lisa.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/officers/philip.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/officers/pranav.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 987 KiB

BIN
public/officers/raymond.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
public/officers/ridhi.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 KiB

BIN
public/officers/rohil.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

BIN
public/officers/shing.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/officers/shipra.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

BIN
public/officers/stella.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

BIN
public/officers/steph.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

BIN
public/officers/terri.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
public/officers/zarif.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

BIN
public/project.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

BIN
public/robocup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

BIN
public/signal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
public/supercomp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,166 +1,3 @@
---
const { filters, currentFilter } = Astro.props;
---
<div class="inline-flex border border-white/20 rounded-full md:p-[0.2vw] p-[0.4vw] relative my-[3vw]">
<div
id="slider"
class="absolute h-[calc(100%-15%)] bg-[#FFB81C] rounded-full transition-none"
style="left: 1%;"
>
</div>
{
filters.map((filter) => (
<button
data-filter={filter}
class={`md:text-[1.3vw] text-[2.5vw] md:px-[1.8vw] px-[3vw] md:py-[0.2vw] py-[0.4vw] rounded-full transition-all relative z-10 ${
currentFilter === filter
? "text-black"
: "text-white hover:bg-white/10 hover:bg-opacity-50"
}`}
>
{filter}
</button>
))
}
</div>
<script>
const buttons = document.querySelectorAll("[data-filter]");
const officers = document.querySelectorAll("[data-officer]");
const container = officers[0]?.parentElement;
const slider = document.getElementById("slider");
// Define type order for consistent sorting
const typeOrder = ["Executives", "Internal", "Events", "Projects"];
function getTypeWeight(type) {
const index = typeOrder.indexOf(type);
return index === -1 ? typeOrder.length : index;
}
function sortOfficersByType() {
const officerArray = Array.from(officers);
officerArray.sort((a, b) => {
const aTypes = JSON.parse(a.getAttribute("data-types"));
const bTypes = JSON.parse(b.getAttribute("data-types"));
return getTypeWeight(aTypes[0]) - getTypeWeight(bTypes[0]);
});
officerArray.forEach((officer) => {
container.appendChild(officer);
});
}
function moveSlider(button) {
if (!slider) return;
const buttonRect = button.getBoundingClientRect();
const containerRect = button.parentElement.getBoundingClientRect();
slider.style.width = `${buttonRect.width}px`;
slider.style.left = `${buttonRect.left - containerRect.left}px`;
}
function updateFilter(selectedFilter, clickedButton) {
// Update button styles
buttons.forEach((btn) => {
const isSelected =
btn.getAttribute("data-filter") === selectedFilter;
btn.classList.toggle("text-black", isSelected);
btn.classList.toggle("text-white", !isSelected);
btn.classList.toggle("hover:bg-white/10", !isSelected);
btn.classList.toggle("hover:bg-opacity-50", !isSelected);
});
// move slider
moveSlider(clickedButton);
// fades out all officers
officers.forEach((officer) => {
officer.style.opacity = "0";
officer.style.transition = "opacity 300ms ease-out";
});
// waits, then removes and re-adds officers
setTimeout(() => {
// removes all officers from container
officers.forEach((officer) => {
officer.remove();
});
// filters officers and prepares them for re-insertion
const officersToShow = Array.from(officers).filter((officer) => {
const types = JSON.parse(
officer.getAttribute("data-types") || "[]"
);
return (
selectedFilter === "All" || types.includes(selectedFilter)
);
});
// sorts if needed
if (selectedFilter === "All") {
officersToShow.sort((a, b) => {
const aTypes = JSON.parse(a.getAttribute("data-types"));
const bTypes = JSON.parse(b.getAttribute("data-types"));
return getTypeWeight(aTypes[0]) - getTypeWeight(bTypes[0]);
});
}
// sets initial opacity to 0 for fade in
officersToShow.forEach((officer) => {
officer.style.opacity = "0";
officer.style.display = "";
container.appendChild(officer);
});
// triggers reflow and fades in
requestAnimationFrame(() => {
officersToShow.forEach((officer) => {
officer.style.opacity = "1";
});
});
}, 300); // matches fade-out duration
}
sortOfficersByType();
// init
const initialButton = Array.from(buttons).find(
(btn) => btn.getAttribute("data-filter") === "All"
);
// init
if (initialButton && slider) {
const buttonRect = initialButton.getBoundingClientRect();
slider.style.width = `${buttonRect.width}px`;
// turns on transitions
requestAnimationFrame(() => {
slider.classList.remove("transition-none");
slider.classList.add(
"transition-all",
"duration-300",
"ease-in-out"
);
});
}
// reveals officers after sorting with animation
requestAnimationFrame(() => {
officers.forEach((officer) => {
officer.style.transition = "opacity 300ms ease-out";
officer.style.visibility = "visible";
// triggers reflow
officer.offsetHeight;
officer.style.opacity = "1";
});
});
// addss click handlers
buttons.forEach((button) => {
button.addEventListener("click", () => {
const filterValue = button.getAttribute("data-filter");
updateFilter(filterValue, button);
});
});
</script>
---

View file

@ -2,55 +2,32 @@
import { FaGear } from "react-icons/fa6";
import { MdEmail } from "react-icons/md";
import Link from "next/link";
import { Image } from "astro:assets";
const { name, position, picture, email } = Astro.props;
const {name, position, picture, email} = Astro.props;
---
<div class = "text-white">
<div class="text-white">
<div class="text-ieee-yellow">
<Link
href={`mailto:${email}`}
className="flex items-center ml-[3%] py-[0.5vh] group"
>
<MdEmail
className="md:text-[1.5vw] text-[2.5vw] mr-[0.5%] group-hover:scale-110 group-hover:opacity-70 duration-300"
/>
<p class="md:text-[0.8vw] text-[2vw]">
<div class = "text-ieee-yellow">
<Link href="s1hung@ucsd.edu" className = "flex items-center ml-[3%] py-[0.5vh]">
<MdEmail className = "text-[1.5vw] mr-[0.5%]"/>
<p class = "text-[0.8vw]">
{email}
</p>
</Link>
</div>
<div
class="md:w-[20vw] w-[35vw] aspect-[334/440] bg-gradient-to-t from-ieee-blue-100/5 to-ieee-blue-100/25 rounded-[10%] flex flex-col items-center relative"
>
<Image
src={picture}
alt="officer"
class="md:w-[18vw] w-[31vw] md:rounded-[1.5vw] rounded-[3vw] mt-[5%] mb-[3%]"
width={334}
height={440}
/>
<div
class="bg-white w-fit rounded-full aspect-square md:p-[0.4vw] p-[0.8vw] text-ieee-black md:text-[1.8vw] text-[3.5vw] absolute md:right-[1.5vw] md:top-[1.5vw] right-[2.5vw] top-[2.5vw]"
>
<FaGear />
<div class = "w-[20vw] aspect-[334/440] bg-gradient-to-t from-ieee-blue-100/5 to-ieee-blue-100/25 rounded-[10%] flex flex-col items-center">
<img src ={picture} alt = "officer" class = "w-[18vw] rounded-[10%] pt-[5%] pb-[3%] relative" >
<div class = "bg-white w-fit rounded-full aspect-square p-[0.4vw] text-ieee-black text-[1.8vw] absolute ml-[13.5vw] mt-[3vh]">
<FaGear/>
</div>
<div
class="flex w-[85%] justify-between"
>
<p
data-inview
class="opacity-0 in-view:animate-fade-right md:text-[1.8vw] text-[3.5vw] font-light md:leading-[2.5vw] leading-[5vw] md:w-[10vw]"
>
<div class = "w-full flex justify-between px-[7%]">
<p class = "text-[2vw] font-light leading-[4.5vh]">
{name}
</p>
<div
data-inview
class="md:mt-[0.5vw] mt-[1.5vw] in-view:animate-fade-up md:text-[0.8vw] text-[1.5vw] w-fit border-[0.11vw] border-white/90 rounded-full px-[1vw] py-[0.1vw] h-fit text-center"
>
<div class = "text-[0.8vw] w-[8vw] border-[0.11vw] border-white/90 rounded-full p-[0.5%] h-fit text-center">
{position}
</div>
</div>
</div>
</div>
</div>

View file

@ -1,64 +1,43 @@
---
import about from "../../images/about.webp";
import about from "../../images/about.png";
import { Image } from "astro:assets";
import neko from "../../images/neko.webp";
import neko from "../../images/neko.png";
import { LiaDotCircle } from "react-icons/lia";
import Officer from "../board/Officer.astro";
import Filter from "../board/Filter.astro";
import officers from "../../data/officers.json";
// Get all unique types and add 'All' option
const typeOrder = ["Executives", "Internal", "Events", "Projects"];
const types = ["All", ...typeOrder];
const currentFilter = "All";
---
<div
class="text-white flex flex-col items-center md:mt-[5vw] mt-[10vw] mb-[10vh]"
>
<div
data-inview
class="relative w-[40vw] md:w-[21vw] mb-[10vh] in-view:animate-fade-down"
>
<Image src={about} alt="About background image" />
<Image
src={neko}
alt="About image"
class="absolute top-[10%] left-[16%] aspect-[399/491] object-cover w-[27vw] md:w-[14vw] rounded-[2vw]"
/>
</div>
<div class="text-white flex flex-col items-center mt-[15vh] mb-[10vh]">
<div class="relative w-[21vw]">
<Image src={about} alt="About background image" />
<Image
src={neko}
alt="About image"
class="absolute top-[10%] left-[16%] aspect-[399/491] object-cover w-[14vw] rounded-[2vw]"
/>
</div>
<div class="text-[5vw] md:text-[2.5vw] flex items-center mt-[1vw]">
<LiaDotCircle className="mr-[1vw] pt-[0.5%]" />
<p>MEET THE BOARD</p>
</div>
<div class="text-[2.5vw] flex items-center mt-[2vh]">
<LiaDotCircle className="mr-[1vw] pt-[0.5%]" />
<p>MEET THE BOARD</p>
</div>
<p
class="md:text-[1.3vw] text-[2.5vw] md:w-[56%] w-[70%] my-[3%] font-extralight text-center"
>
Our board comprises 31 students of varying majors, colleges, and interests!
Feel free to reach out for any questions about our position or experiences.
</p>
<p class="text-[1.3vw] w-[56%] my-[3%] font-extralight text-center">
Erat hendrerit tristique erat; parturient cursus fringilla feugiat. Eget
faucibus fames ridiculus nec egestas convallis cubilia malesuada. Tellus
nibh vivamus tempus molestie tristique quis
</p>
<Filter filters={types} currentFilter={currentFilter} />
<div class="grid gap-[3vw] md:grid-cols-3 grid-cols-2 mt-[2vh]">
{
officers.map((officer) => (
<div
data-officer
data-types={JSON.stringify(officer.type)}
style="opacity: 0; visibility: hidden"
>
<Officer
name={officer.name}
position={officer.position}
picture={officer.picture}
email={officer.email}
/>
</div>
))
}
</div>
<div class="grid gap-[3vw] grid-cols-3 mt-[10vh]">
{
officers.map((officer) => (
<Officer
name={officer.name}
position={officer.position}
picture={officer.picture}
email={officer.email}
/>
))
}
</div>
</div>

View file

@ -1,19 +1,17 @@
---
import { LiaDotCircle } from "react-icons/lia";
const { title, text } = Astro.props;
const {title, text} = Astro.props;
---
<div class="flex flex-col items-center text-white my-[10%]">
<div class="flex items-center text-[4.5vw] md:text-[2.5vw] mb-[3%]">
<LiaDotCircle className=" mr-[1vw] pt-[0.5%]" />
<p
class="text-transparent bg-clip-text bg-gradient-to-b from-white via-white to-ieee-black"
>
{title}
</p>
</div>
<div class="flex items-center text-[2.5vw] mb-[3%]">
<LiaDotCircle className=" mr-[1vw] pt-[0.5%]"/>
<p class="text-transparent bg-clip-text bg-gradient-to-b from-white via-white to-ieee-black">
{title}
</p>
</div>
<p class="w-[70%] md:text-[1.4vw] text-[2vw] font-light">
{text}
</p>
</div>
<p class="w-[70%] text-[1.4vw] font-light ">
{text}
</p>
</div>

View file

@ -5,22 +5,22 @@ import whiteLogoHorizontal from "../../images/logos/white_logo_horizontal.svg";
import { IoHeart } from "react-icons/io5";
---
<div class="w-full md:py-[2%] py-[3vw] flex justify-center">
<div class="w-full py-[2%] flex justify-center">
<div
class="w-[95%] bg-black/40 md:py-[1%] py-[2vw] px-[1.5%] rounded-[2vw] border-[0.1vw] flex justify-between"
class="w-[95%] bg-black/40 py-[1%] px-[1.5%] rounded-[2vw] border-[0.1vw] flex justify-between"
>
<Link href="/" className="hover:opacity-70 duration-300">
<Image
class="md:w-[15vw] w-[35vw]"
class="w-[15vw]"
src={whiteLogoHorizontal}
alt="IEEE UCSD Logo"
/>
</Link>
<div class="flex items-center">
<p class="text-white md:text-[1.2vw] text-[2.7vw]">
<p class="text-white text-[1.2vw]">
made by IEEE UCSD webmasters with
</p>
<IoHeart className=" text-ieee-blue-100 ml-[1vw] md:text-[1.5vw] text-[4vw]" />
<IoHeart className=" text-ieee-blue-100 ml-[1vw] text-[1.5vw]" />
</div>
</div>
</div>

View file

@ -1,22 +1,20 @@
<script>
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("in-view");
entry.target.classList.remove("opacity-0");
// console.log("Added 'in-view' class to:", entry.target);
} else {
entry.target.classList.remove("in-view");
entry.target.classList.add("opacity-0");
// console.log("Removed 'in-view' class from:", entry.target);
}
});
},
{ threshold: 0.2 },
);
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("in-view");
console.log("Added 'in-view' class to:", entry.target);
} else {
entry.target.classList.remove("in-view");
console.log("Removed 'in-view' class from:", entry.target);
}
});
},
{ threshold: 0.2 }
);
document
.querySelectorAll("[data-inview]")
.forEach((el) => observer.observe(el));
document
.querySelectorAll("[data-inview]")
.forEach((el) => observer.observe(el));
</script>

View file

@ -4,385 +4,146 @@ import whiteLogoHorizontal from "../../images/logos/white_logo_horizontal.svg";
import pages from "../../data/pages.json";
---
<div class="w-full">
<div
class="flex justify-between items-center bg-ieee-black my-[1%] mx-[2.5%] py-[0.5%] px-[1%] rounded-full md:border-[0.1vw]"
>
<a href="/" class="hover:opacity-60 duration-300">
<Image
class="w-[15vw] md:block hidden"
src={whiteLogoHorizontal}
alt="IEEE UCSD Logo"
/>
<Image
class="w-[40vw] md:hidden block"
src={whiteLogoHorizontal}
alt="IEEE UCSD Logo"
/>
</a>
<!-- Desktop Navigation -->
<div class="hidden md:flex md:w-[55%] md:justify-between">
{
pages.map((page) =>
page.subpages ? (
<div class="relative group">
<a
href={page.path}
class="uppercase rounded-full duration-300 px-[1.5vw] py-[0.2vw] text-[1.2vw] text-nowrap text-white border-white hover:opacity-50 border-[0.1vw] font-light inline-block"
>
{page.name}
</a>
<div class="absolute left-1/2 transform -translate-x-1/2 w-[12vw] top-full z-50">
<div class="rounded-lg bg-ieee-black border border-white shadow-lg hidden group-hover:block animate-fade-down animate-duration-200 animate-ease-in-out">
{page.subpages.map((subpage) => (
<a
href={subpage.path}
class="font-light py-[4%] block text-white hover:bg-gray-700 text-[1.3vw] first:rounded-t-lg last:rounded-b-lg text-center whitespace-nowrap transition-colors duration-200"
>
{subpage.name}
</a>
))}
</div>
</div>
</div>
) : (
<a
href={page.path}
class={`uppercase rounded-full duration-300 px-[1.5vw] py-[0.2vw] text-[1.2vw] text-nowrap
${
page.name === "Dashboard"
? "bg-ieee-yellow text-black hover:opacity-70"
: "text-white border-white hover:opacity-50 border-[0.1vw] font-light"
}`}
>
{page.name}
</a>
),
)
}
</div>
<!-- Mobile Hamburger/Close Button -->
<button
id="menu-btn"
class="md:hidden text-white p-2 flex justify-center items-center focus:outline-none relative z-[60] scale-150"
aria-label="Toggle menu"
>
<!-- Hamburger Icon -->
<svg
class="w-6 h-6 menu-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<!-- Close Icon -->
<svg
class="w-6 h-6 close-icon hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Mobile Menu -->
<div
id="mobile-menu"
class="fixed inset-0 z-[51] hidden xl:hidden motion-safe:transition-transform motion-safe:duration-300 translate-x-full bg-black"
>
<div class="md:w-full w-fit fixed z-10">
<div
class="flex flex-col items-center min-h-screen w-full justify-center py-20 px-4 space-y-6 overflow-y-auto"
class="flex justify-between items-center bg-black my-[1%] mx-[2.5%] py-[0.5%] px-[1%] md:rounded-full md:border-[0.1vw]"
>
{
pages.map((page) =>
page.subpages ? (
<div class="w-full max-w-md space-y-4 relative dropdown-container">
<div class="flex gap-2">
<a
href={page.path}
class="flex-1 block py-4 px-12 text-center rounded-[3rem] motion-safe:transition-colors motion-safe:duration-200 uppercase font-bold text-2xl text-white hover:text-gray-300 border-white border-2"
>
{page.name}
</a>
<button
class="mobile-dropdown-toggle py-4 px-6 text-center rounded-[3rem] motion-safe:transition-all motion-safe:duration-200 uppercase font-bold text-2xl text-white hover:text-gray-300 border-white border-2 flex items-center justify-center"
aria-expanded="false"
>
<svg
class="w-6 h-6 transform transition-transform duration-200 dropdown-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
<div class="mobile-dropdown-content mt-2 space-y-2 pl-4 absolute w-full animate-duration-200 animate-ease-in-out">
{page.subpages.map((subpage) => (
<a
href={subpage.path}
class="block py-3 px-8 text-center rounded-[2rem] motion-safe:transition-all motion-safe:duration-200 uppercase font-medium text-lg w-full text-white hover:text-gray-300 border-white border bg-[#111111] hover:bg-[#222222]"
>
{subpage.name}
</a>
))}
</div>
</div>
) : (
<a
href={page.path}
class={`block py-4 px-12 text-center rounded-[3rem] motion-safe:transition-colors motion-safe:duration-200 uppercase font-bold text-2xl w-full max-w-md
${
page.name === "Dashboard"
? "bg-[#f3c135] text-black border-[#f3c135] hover:bg-[#dba923] hover:border-[#dba923]"
: "text-white hover:text-gray-300 border-white border-2"
}`}
<a href="/" class="hover:opacity-60 duration-300 hidden md:flex">
<Image
class="w-[15vw]"
src={whiteLogoHorizontal}
alt="IEEE UCSD Logo"
/>
</a>
<!-- Desktop Navigation -->
<div class="hidden md:flex md:w-[55%] md:justify-between">
{
pages.map((page) => (
<a
href={page.path}
class={`uppercase rounded-full duration-300 px-[1.5vw] py-[0.2vw] text-[1.2vw] text-nowrap
${
page.name === "Online Store"
? "bg-ieee-yellow text-black hover:opacity-70"
: "text-white border-white hover:opacity-50 border-[0.1vw] font-light"
}`}
>
{page.name}
</a>
))
}
</div>
<!-- Mobile Hamburger/Close Button -->
<button
id="menu-btn"
class="md:hidden text-white p-2 flex justify-center items-center focus:outline-none relative z-[60] scale-150"
aria-label="Toggle menu"
>
<!-- Hamburger Icon -->
<svg
class="w-6 h-6 menu-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{page.name}
</a>
),
)
}
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<!-- Close Icon -->
<svg
class="w-6 h-6 close-icon hidden"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Mobile Menu -->
<div
id="mobile-menu"
class="fixed inset-0 z-[51] hidden xl:hidden motion-safe:transition-transform motion-safe:duration-300 translate-x-full"
>
<div
class="flex flex-col items-center h-[70vh] justify-evenly bg-black"
>
{
pages.map((page) => (
<a
href={page.path}
class={`block py-2 px-8 text-center rounded-[3rem] motion-safe:transition-colors motion-safe:duration-200 uppercase font-bold text-xl
${
page.name === "Online Store"
? "bg-[#f3c135] text-black border-[#f3c135] hover:bg-[#dba923] hover:border-[#dba923]"
: "text-white hover:text-gray-300 border-white"
}`}
>
{page.name}
</a>
))
}
</div>
</div>
</div>
</div>
<style>
#mobile-menu.show {
@apply translate-x-0;
}
.mobile-dropdown-content {
opacity: 0;
pointer-events: none;
z-index: 30;
width: calc(100% - 1rem) !important;
animation: fadeOut 0.2s ease-in-out forwards;
}
.mobile-dropdown-content.show {
opacity: 1;
pointer-events: all;
backdrop-filter: none;
animation: fadeIn 0.2s ease-in-out forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
#mobile-menu.show {
@apply translate-x-0;
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}
.mobile-dropdown-content a {
margin-inline: auto;
width: 100% !important;
max-width: calc(100% - 1rem);
}
.dropdown-icon.rotated {
transform: rotate(180deg);
}
/* Add styles for focus effect */
.dropdown-container {
transition: all 0.3s ease;
position: relative;
isolation: isolate;
}
.dropdown-container.active {
transform: scale(1.02);
z-index: 25;
backdrop-filter: none;
filter: none;
transition: all 0.2s ease;
}
.dropdown-container.active .mobile-dropdown-toggle,
.dropdown-container.active > div > a {
position: relative;
z-index: 25;
pointer-events: all;
backdrop-filter: none;
filter: none;
transition: all 0.2s ease;
}
.dropdown-container.active::after {
content: "";
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 15;
pointer-events: none;
transition: all 0.3s ease;
}
#mobile-menu.has-active-dropdown > div > *:not(.dropdown-container.active) {
opacity: 0.2;
transform: scale(0.98);
filter: blur(2px);
transition: all 0.2s ease;
pointer-events: none;
}
#mobile-menu.has-active-dropdown .dropdown-container.active {
pointer-events: all;
filter: none;
transition: all 0.2s ease;
}
#mobile-menu.has-active-dropdown
.dropdown-container.active
.mobile-dropdown-content.show {
filter: none;
backdrop-filter: none;
background: transparent;
}
</style>
<script>
const menuBtn = document.getElementById("menu-btn");
const mobileMenu = document.getElementById("mobile-menu");
const menuIcon = document.querySelector(".menu-icon");
const closeIcon = document.querySelector(".close-icon");
const dropdownToggles = document.querySelectorAll(".mobile-dropdown-toggle");
const menuBtn = document.getElementById("menu-btn");
const mobileMenu = document.getElementById("mobile-menu");
const menuIcon = document.querySelector(".menu-icon");
const closeIcon = document.querySelector(".close-icon");
function toggleMenu(show: boolean) {
if (show) {
mobileMenu?.classList.remove("hidden");
menuIcon?.classList.add("hidden");
closeIcon?.classList.remove("hidden");
document.body.style.overflow = "hidden";
function toggleMenu(show: boolean) {
if (show) {
mobileMenu?.classList.remove("hidden");
menuIcon?.classList.add("hidden");
closeIcon?.classList.remove("hidden");
document.body.style.overflow = "hidden";
setTimeout(() => {
mobileMenu?.classList.add("show");
}, 10);
} else {
mobileMenu?.classList.remove("show");
menuIcon?.classList.remove("hidden");
closeIcon?.classList.add("hidden");
document.body.style.overflow = "";
setTimeout(() => {
mobileMenu?.classList.add("show");
}, 10);
} else {
mobileMenu?.classList.remove("show");
menuIcon?.classList.remove("hidden");
closeIcon?.classList.add("hidden");
document.body.style.overflow = "";
// First wait for the navbar to slide out
setTimeout(() => {
mobileMenu?.classList.add("hidden");
// Then reset all dropdowns and focus states
document.querySelectorAll(".dropdown-container").forEach((el) => {
const dropdownContent = el.querySelector(".mobile-dropdown-content");
const dropdownToggle = el.querySelector(".mobile-dropdown-toggle");
const dropdownIcon = dropdownToggle?.querySelector(".dropdown-icon");
el.classList.remove("active");
dropdownContent?.classList.remove("show");
dropdownIcon?.classList.remove("rotated");
dropdownToggle?.setAttribute("aria-expanded", "false");
});
mobileMenu?.classList.remove("has-active-dropdown");
}, 300);
setTimeout(() => {
mobileMenu?.classList.add("hidden");
}, 100);
}
}
}
menuBtn?.addEventListener("click", () => {
const isMenuHidden = mobileMenu?.classList.contains("hidden") ?? true;
toggleMenu(isMenuHidden);
});
// Handle dropdown toggles
dropdownToggles.forEach((toggle) => {
toggle.addEventListener("click", (e) => {
e.stopPropagation();
const container = toggle.closest(".dropdown-container");
const content = toggle.parentElement?.nextElementSibling as HTMLElement;
const icon = toggle.querySelector(".dropdown-icon");
const isExpanded = toggle.getAttribute("aria-expanded") === "true";
// If clicking an already active dropdown, close it and restore interactions
if (isExpanded) {
// First close the dropdown
content?.classList.remove("show");
icon?.classList.remove("rotated");
// Then wait for animation to complete plus a delay before removing focus
setTimeout(() => {
container?.classList.remove("active");
mobileMenu?.classList.remove("has-active-dropdown");
toggle.setAttribute("aria-expanded", "false");
}, 300); // 200ms for animation + 100ms delay
return;
}
// Remove active state from all dropdowns first
document.querySelectorAll(".dropdown-container").forEach((el) => {
const dropdownContent = el.querySelector(".mobile-dropdown-content");
const dropdownToggle = el.querySelector(".mobile-dropdown-toggle");
const dropdownIcon = dropdownToggle?.querySelector(".dropdown-icon");
dropdownContent?.classList.remove("show");
dropdownIcon?.classList.remove("rotated");
dropdownToggle?.setAttribute("aria-expanded", "false");
});
// First focus the new dropdown
container?.classList.add("active");
mobileMenu?.classList.add("has-active-dropdown");
toggle.setAttribute("aria-expanded", "true");
icon?.classList.add("rotated");
// Then show the content with animation after a short delay
setTimeout(() => {
requestAnimationFrame(() => {
content?.classList.add("show");
});
}, 100);
menuBtn?.addEventListener("click", () => {
const isMenuHidden = mobileMenu?.classList.contains("hidden") ?? true;
toggleMenu(isMenuHidden);
});
});
// Close menu when clicking outside ~ Doesnt really work at the moment
document.addEventListener("click", (e) => {
if (
!mobileMenu?.contains(e.target as Node) &&
!menuBtn?.contains(e.target as Node) &&
!mobileMenu?.classList.contains("hidden")
) {
toggleMenu(false);
}
});
// Close menu when clicking outside
document.addEventListener("click", (e) => {
if (
!mobileMenu?.contains(e.target as Node) &&
!menuBtn?.contains(e.target as Node) &&
!mobileMenu?.classList.contains("hidden")
) {
toggleMenu(false);
}
});
</script>

View file

@ -1,11 +1,11 @@
---
const {question, answer} = Astro.props;
---
<div class = "text-white w-full flex flex-col mb-[2vh] animate-ease-in-out">
<p data-inview class = "in-view:animate-fade-right text-ieee-yellow text-[2.5vw] md:text-[1.4vw] mb-[2vh] font-semibold pl-[1vw]">
<div class = "text-white w-full flex flex-col mb-[2vh]">
<p class = "text-ieee-yellow text-[1.4vw] mb-[2vh] font-semibold pl-[1vw]">
{question}
</p>
<p data-inview class = "md:w-[70%] w-[80%] mb-[2vh] pl-[1vw] text-[2vw] md:text-[1.2vw] in-view:animate-fade-left animate-ease-in-out">
<p class = "w-[60%] mb-[2vh] pl-[1vw]">
{answer}
</p>
<dev>

View file

@ -4,8 +4,8 @@ import { LiaDotCircle } from "react-icons/lia";
const {faq} = Astro.props;
---
<div class = "text-white md:ml-[15vw] ml-[10vw] mb-[20%] mt-[10%]">
<div class = "text-[4.5vw] md:text-[2.5vw] flex items-center my-[7vh]">
<div class = "text-white w-full h-[90vh] ml-[15vw] mb-[20%] mt-[10%]">
<div class = "text-[2.5vw] flex items-center my-[7vh]">
<LiaDotCircle className = "mr-[1vw]" />
<p>
Frequently Asked Questions

View file

@ -1,79 +1,70 @@
---
import { LiaDotCircle } from "react-icons/lia";
import { Image } from "astro:assets";
import jellyfish from "../../images/jellyfish.png";
import { FaDiscord } from "react-icons/fa";
import evan from "../../images/evan.png";
import { FaDiscord, FaFacebook } from "react-icons/fa";
import { RiInstagramFill } from "react-icons/ri";
import { MdEmail } from "react-icons/md";
import Link from "next/link";
---
<div
class="text-white flex flex-col items-center md:h-[35vw] h-[60vw] justify-between"
>
<div class="flex items-center text-[4.5vw] md:text-[2.5vw]">
<div class="text-white flex flex-col items-center h-[65vh] justify-between">
<div class="flex items-center text-[2.5vw]">
<LiaDotCircle className=" mr-[1vw] pt-[0.5%]" />
<p>Social Media</p>
</div>
<p class="md:w-1/3 w-3/5 text-center text-[2vw] md:text-[1.2vw] pb-[5%]">
Stay connected with us on Discord, Facebook, and Instagram! We regularly
post information on upcoming events and competitions.
<p class="w-1/3 text-center text-[1.2vw] pb-[5%]">
Lorem ipsum is placeholder text commonly used in the graphic, print, and
publishing industries f
</p>
<div
class="md:w-[85%] w-full rounded-[3vw] bg-gradient-to-r from-ieee-blue-300 to-ieee-blue-100 relative md:h-[15vw] h-[30vw] flex items-center text-white/90"
class="w-[85%] rounded-[3vw] bg-gradient-to-r from-ieee-blue-300 to-ieee-blue-100 relative h-[30vh] flex items-center text-white/90"
>
<div
data-inview
class="md:w-2/5 w-1/2 flex justify-evenly ml-[5%] animate-ease-in-out in-view:animate-flip-up animate-duration-1000"
>
<Link
href="https://www.instagram.com/ieee.ucsd"
target="_blank"
className="flex flex-col items-center"
>
<div
class="border-[0.15vw] rounded-full shadow-glow hover:scale-110 duration-300 p-[1vw] text-[5vw] md:text-[2.2vw] text-ieee-black/80 border-ieee-blue-100 bg-gradient-radial from-white to-ieee-blue-100"
>
<RiInstagramFill />
</div>
<p class="text-[2vw] md:text-[1.3vw] font-semibold">
Instagram
</p>
</Link>
<div class="w-2/5 flex justify-evenly ml-[5%]">
<Link
href="https://discord.gg/XxfjqZSjca"
target="_blank"
className="flex flex-col items-center"
>
<div
class="border-[0.15vw] rounded-full shadow-glow hover:scale-110 duration-300 p-[1vw] text-[5vw] md:text-[2.2vw] text-ieee-black/80 border-ieee-blue-100 bg-gradient-radial from-white to-ieee-blue-100"
class="border-[0.15vw] rounded-full shadow-glow hover:scale-110 duration-300 p-[1vw] text-[2.2vw] text-ieee-black/80 border-ieee-blue-100 bg-gradient-radial from-white to-ieee-blue-100"
>
<FaDiscord />
</div>
<p class="text-[2vw] md:text-[1.3vw] font-semibold">Discord</p>
<p class="text-[1.3vw] font-semibold">Discord</p>
</Link>
<Link
href="mailto:ieee@ucsd.edu"
href="https://www.facebook.com/ieeeucsd"
target="_blank"
className="flex flex-col items-center"
>
<div
class="border-[0.15vw] rounded-full shadow-glow hover:scale-110 duration-300 p-[1vw] text-[5vw] md:text-[2.2vw] text-ieee-black/80 border-ieee-blue-100 bg-gradient-radial from-white to-ieee-blue-100"
class="border-[0.15vw] rounded-full shadow-glow hover:scale-110 duration-300 p-[1vw] text-[2.2vw] text-ieee-black/80 border-ieee-blue-100 bg-gradient-radial from-white to-ieee-blue-100"
>
<MdEmail />
<FaFacebook />
</div>
<p class="text-[2vw] md:text-[1.3vw] font-semibold">Email</p>
<p class="text-[1.3vw] font-semibold">Facebook</p>
</Link>
<Link
href="https://www.instagram.com/ieee.ucsd"
target="_blank"
className="flex flex-col items-center"
>
<div
class="border-[0.15vw] rounded-full shadow-glow hover:scale-110 duration-300 p-[1vw] text-[2.2vw] text-ieee-black/80 border-ieee-blue-100 bg-gradient-radial from-white to-ieee-blue-100"
>
<RiInstagramFill />
</div>
<p class="text-[1.3vw] font-semibold">Instagram</p>
</Link>
</div>
<Image
data-inview
src={jellyfish}
alt="Jellyfish mascot"
class="absolute bottom-[2vw] md:w-[25vw] w-[35vw] right-[4vw] animate-wiggle animate-infinite"
src={evan}
alt="cat placeholder"
class="absolute -bottom-[10vh] w-[25vw] right-0"
/>
</div>
</div>

View file

@ -1,11 +1,11 @@
---
const { title } = Astro.props;
const {title} = Astro.props;
import { LiaDotCircle } from "react-icons/lia";
---
<div class="flex items-center md:text-[2.5vw] text-[4vw] mb-[5%]">
<LiaDotCircle className=" mr-[1vw] pt-[0.5%]" />
<p>
{title}
</p>
</div>
<div class="flex items-center text-[2.5vw] mb-[5%]">
<LiaDotCircle className=" mr-[1vw] pt-[0.5%]"/>
<p>
{title}
</p>
</div>

View file

@ -1,13 +1,11 @@
---
const { title } = Astro.props;
const {title} = Astro.props;
import { LiaDotCircle } from "react-icons/lia";
---
<div
class="flex items-center md:text-[3vw] text-[4.5vw] ml-[10%] md:pt-[5%] pt-[10%] text-white font-semibold"
>
<LiaDotCircle className=" mr-[1vw] text-[2.7vw]" />
<p>
{title}
</p>
</div>
<div class="flex items-center text-[3vw] ml-[10%] pt-[10%] text-white font-semibold">
<LiaDotCircle className=" mr-[1vw] text-[2.7vw]"/>
<p>
{title}
</p>
</div>

View file

@ -1,3 +0,0 @@
---
---

View file

@ -1,533 +0,0 @@
---
import FilePreview from "./universal/FilePreview";
import EventCheckIn from "./EventsSection/EventCheckIn";
import EventLoad from "./EventsSection/EventLoad";
---
<div id="" class="">
<div class="mb-4 sm:mb-6 px-4 sm:px-6">
<h2 class="text-xl sm:text-2xl font-bold">Events</h2>
<p class="opacity-70 text-sm sm:text-base">
View and manage your IEEE UCSD events
</p>
</div>
<div
class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-4 sm:mb-6 px-4 sm:px-6"
>
<!-- Event Check-in Card -->
<div class="w-full">
<EventCheckIn client:load />
</div>
<!-- Event Registration Card -->
<div class="w-full">
<div
class="card bg-base-100 shadow-xl border border-base-200 opacity-50 cursor-not-allowed relative group h-full"
>
<div
class="absolute inset-0 bg-base-100 opacity-0 group-hover:opacity-90 transition-opacity duration-300 flex items-center justify-center z-10"
>
<span class="text-base-content font-medium text-sm sm:text-base"
>Coming Soon</span
>
</div>
<div class="card-body p-4 sm:p-6">
<h3 class="card-title text-base sm:text-lg mb-3 sm:mb-4">
Event Registration
</h3>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-sm sm:text-base"
>Select an event to register</span
>
</label>
<div class="flex flex-col sm:flex-row gap-2">
<select
class="select select-bordered flex-1 text-sm sm:text-base h-10 min-h-[2.5rem] w-full"
disabled
>
<option disabled selected>Pick an event</option>
<option>Technical Workshop - Web Development</option>
<option>Professional Development Workshop</option>
<option>Social Event - Game Night</option>
</select>
<button
class="btn btn-primary text-sm sm:text-base h-10 min-h-[2.5rem] w-full sm:w-[100px]"
disabled>Register</button
>
</div>
</div>
</div>
</div>
</div>
</div>
<EventLoad client:load />
</div>
<!-- Event Details Modal -->
<dialog id="eventDetailsModal" class="modal">
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
<div class="flex justify-between items-center mb-3 sm:mb-4">
<div class="flex items-center gap-2 sm:gap-3">
<h3 class="font-bold text-base sm:text-lg" id="modalTitle">
Event Files
</h3>
<button
id="downloadAllBtn"
class="btn btn-primary btn-sm gap-1 text-xs sm:text-sm"
onclick="window.downloadAllFiles()"
>
<iconify-icon
icon="heroicons:arrow-down-tray-20-solid"
className="h-3 w-3 sm:h-4 sm:w-4"></iconify-icon>
Download All
</button>
</div>
<button
class="btn btn-circle btn-ghost btn-sm sm:btn-md"
onclick="window.closeEventDetailsModal()"
>
<iconify-icon icon="heroicons:x-mark" className="h-4 w-4 sm:h-6 sm:w-6"
></iconify-icon>
</button>
</div>
<div id="filesContent" class="space-y-3 sm:space-y-4">
<!-- Files list will be populated here -->
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick="window.closeEventDetailsModal()">close</button>
</form>
</dialog>
<!-- Universal File Preview Modal -->
<dialog id="filePreviewModal" class="modal">
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
<div class="flex justify-between items-center mb-3 sm:mb-4">
<div class="flex items-center gap-2 sm:gap-3">
<button
class="btn btn-ghost btn-sm text-xs sm:text-sm"
onclick="window.closeFilePreviewEvents()">Close</button
>
<h3
class="font-bold text-base sm:text-lg truncate"
id="previewFileName"
>
</h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="previewLoadingSpinner"
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
>
<span class="loading loading-spinner loading-md sm:loading-lg"></span>
</div>
<div id="previewContent" class="w-full">
<FilePreview client:load isModal={true} />
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button onclick="window.closeFilePreviewEvents()">close</button>
</form>
</dialog>
<script>
import { toast } from "react-hot-toast";
import JSZip from "jszip";
// Add styles to the document
const style = document.createElement("style");
style.textContent = `
/* Custom styles for the event details modal */
.event-details-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 640px) {
.event-details-grid {
grid-template-columns: 1fr;
}
}
/* Remove custom toast styles since we're using react-hot-toast */
`;
document.head.appendChild(style);
// Add helper functions for file preview
function getFileType(filename: string): string {
const extension = filename.split(".").pop()?.toLowerCase();
const mimeTypes: { [key: string]: string } = {
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
mp4: "video/mp4",
mp3: "audio/mpeg",
txt: "text/plain",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
json: "application/json",
};
return mimeTypes[extension || ""] || "application/octet-stream";
}
// Universal file preview function for events section
window.previewFileEvents = function (url: string, filename: string) {
// console.log("previewFileEvents called with:", { url, filename });
// console.log("URL type:", typeof url, "URL length:", url?.length || 0);
// console.log(
// "Filename type:",
// typeof filename,
// "Filename length:",
// filename?.length || 0
// );
// Validate inputs
if (!url || typeof url !== "string") {
console.error("Invalid URL provided to previewFileEvents:", url);
toast.error("Cannot preview file: Invalid URL");
return;
}
if (!filename || typeof filename !== "string") {
console.error(
"Invalid filename provided to previewFileEvents:",
filename,
);
toast.error("Cannot preview file: Invalid filename");
return;
}
// Ensure URL is properly formatted
if (!url.startsWith("http")) {
console.warn("URL doesn't start with http, attempting to fix:", url);
if (url.startsWith("/")) {
url = `https://pocketbase.ieeeucsd.org${url}`;
} else {
url = `https://pocketbase.ieeeucsd.org/${url}`;
}
// console.log("Fixed URL:", url);
}
const modal = document.getElementById(
"filePreviewModal",
) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent");
const loadingSpinner = document.getElementById("previewLoadingSpinner");
if (modal && previewFileName && previewContent) {
// console.log("Found all required elements");
// Show loading spinner
if (loadingSpinner) {
loadingSpinner.classList.remove("hidden");
}
// Update the filename display
previewFileName.textContent = filename;
// Show the modal
modal.showModal();
// Test the URL with a fetch before dispatching the event
fetch(url, { method: "HEAD" })
.then((response) => {
// console.log(
// "URL test response:",
// response.status,
// response.ok
// );
if (!response.ok) {
console.warn("URL might not be accessible:", url);
toast(
"File might not be accessible. Attempting to preview anyway.",
{
icon: "⚠️",
style: {
borderRadius: "10px",
background: "#FFC107",
color: "#000",
},
},
);
}
})
.catch((err) => {
console.error("Error testing URL:", err);
})
.finally(() => {
// Dispatch state change event to update the FilePreview component
// console.log(
// "Dispatching filePreviewStateChange event with:",
// { url, filename }
// );
window.dispatchEvent(
new CustomEvent("filePreviewStateChange", {
detail: { url, filename },
}),
);
});
// Hide loading spinner after a short delay
setTimeout(() => {
if (loadingSpinner) {
loadingSpinner.classList.add("hidden");
}
}, 1000); // Increased delay to allow for URL testing
} else {
console.error("Missing required elements for file preview");
toast.error("Could not initialize file preview");
}
};
// Close file preview for events section
window.closeFilePreviewEvents = function () {
// console.log("closeFilePreviewEvents called");
const modal = document.getElementById(
"filePreviewModal",
) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent");
const loadingSpinner = document.getElementById("previewLoadingSpinner");
if (loadingSpinner) {
loadingSpinner.classList.add("hidden");
}
if (modal && previewFileName && previewContent) {
// console.log("Resetting preview and closing modal");
// First reset the preview state by dispatching an event with empty values
// This ensures the FilePreview component clears its internal state
window.dispatchEvent(
new CustomEvent("filePreviewStateChange", {
detail: { url: "", filename: "" },
}),
);
// Reset the UI
previewFileName.textContent = "";
// Close the modal
modal.close();
// console.log("File preview modal closed and state reset");
} else {
console.error("Could not find elements to close file preview");
}
};
// Update the showFilePreview function for events section
window.showFilePreviewEvents = function (file: {
url: string;
name: string;
}) {
// console.log("showFilePreviewEvents called with:", file);
if (!file || !file.url || !file.name) {
console.error("Invalid file data:", file);
toast.error("Could not preview file: missing file information");
return;
}
window.previewFileEvents(file.url, file.name);
};
// Update the openDetailsModal function to use the events-specific preview
window.openDetailsModal = function (event: any) {
const modal = document.getElementById(
"eventDetailsModal",
) as HTMLDialogElement;
const filesContent = document.getElementById(
"filesContent",
) as HTMLDivElement;
// Check if event has ended
const eventEndDate = new Date(event.end_date);
const now = new Date();
if (eventEndDate > now) {
toast("Files are only available after the event has ended.", {
icon: "⚠️",
style: {
borderRadius: "10px",
background: "#FFC107",
color: "#000",
},
});
return;
}
// Reset state
window.currentEventId = event.id;
if (filesContent) filesContent.classList.remove("hidden");
// Populate files content
if (event.files && Array.isArray(event.files) && event.files.length > 0) {
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = event.id;
filesContent.innerHTML = `
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>File Name</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
${event.files
.map((file: string) => {
// Ensure the file URL is properly formatted
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
const fileType = getFileType(file);
// Properly escape the data for the onclick handler
const fileData = {
url: fileUrl,
name: file,
};
return `
<tr>
<td>${file}</td>
<td class="text-right">
<button class="btn btn-ghost btn-sm" onclick="window.showFilePreviewEvents({'url': '${fileUrl}', 'name': '${file}'})">
<iconify-icon icon="heroicons:document" className="h-4 w-4" />
</button>
<a href="${fileUrl}" download="${file}" class="btn btn-ghost btn-sm">
<iconify-icon icon="heroicons:arrow-down-tray-20-solid" className="h-4 w-4" />
</a>
</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
`;
} else {
filesContent.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<iconify-icon icon="heroicons:document-duplicate" className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No files attached to this event</p>
</div>
`;
}
modal.showModal();
};
// Add downloadAllFiles function
window.downloadAllFiles = async function () {
const downloadBtn = document.getElementById(
"downloadAllBtn",
) as HTMLButtonElement;
if (!downloadBtn) return;
const originalBtnContent = downloadBtn.innerHTML;
try {
// Show loading state
downloadBtn.innerHTML =
'<span class="loading loading-spinner loading-xs"></span> Preparing...';
downloadBtn.disabled = true;
const zip = new JSZip();
// Get current event files
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = window.currentEventId;
// Get the current event from the window object
const eventDataId = `event_${window.currentEventId}`;
const event = window[eventDataId];
if (!event || !event.files || event.files.length === 0) {
throw new Error("No files available to download");
}
// Download each file and add to zip
const filePromises = event.files.map(async (filename: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${filename}`;
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(`Failed to download ${filename}`);
}
const blob = await response.blob();
zip.file(filename, blob);
});
await Promise.all(filePromises);
// Generate and download zip
const zipBlob = await zip.generateAsync({ type: "blob" });
const downloadUrl = URL.createObjectURL(zipBlob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = `${event.event_name}_files.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
// Show success message
toast.success("Files downloaded successfully!");
} catch (error: any) {
console.error("Failed to download files:", error);
toast.error(
error?.message || "Failed to download files. Please try again.",
);
} finally {
// Reset button state
downloadBtn.innerHTML = originalBtnContent;
downloadBtn.disabled = false;
}
};
// Close event details modal
window.closeEventDetailsModal = function () {
const modal = document.getElementById(
"eventDetailsModal",
) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
if (modal) {
// Reset the files content
if (filesContent) {
filesContent.innerHTML = "";
}
// Reset any other state if needed
window.currentEventId = "";
// Close the modal
modal.close();
}
};
// Make helper functions available globally
window.showFilePreview = window.showFilePreviewEvents;
window.handlePreviewError = function () {
const previewContent = document.getElementById("previewContent");
if (previewContent) {
previewContent.innerHTML = `
<div class="alert alert-error">
<Icon icon="heroicons:x-circle" className="h-6 w-6" />
<span>Failed to load file preview</span>
</div>
`;
}
};
</script>

View file

@ -1,567 +0,0 @@
import { useState, useEffect } from "react";
import { Get } from "../../../scripts/pocketbase/Get";
import { Authentication } from "../../../scripts/pocketbase/Authentication";
import { Update } from "../../../scripts/pocketbase/Update";
import { SendLog } from "../../../scripts/pocketbase/SendLog";
import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { Collections } from "../../../schemas/pocketbase/schema";
import { Icon } from "@iconify/react";
import toast from "react-hot-toast";
import type { Event, EventAttendee, LimitedUser } from "../../../schemas/pocketbase";
// Extended Event interface with additional properties needed for this component
interface ExtendedEvent extends Event {
description?: string; // This component uses 'description' but schema has 'event_description'
event_type: string; // Add event_type field from schema
}
// Note: Date conversion is now handled automatically by the Get and Update classes.
// When fetching events, UTC dates are converted to local time.
// When saving events, local dates are converted back to UTC.
const EventCheckIn = () => {
const [currentCheckInEvent, setCurrentCheckInEvent] = useState<Event | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [foodInput, setFoodInput] = useState("");
// SECURITY FIX: Purge event codes when component mounts
useEffect(() => {
const dataSync = DataSyncService.getInstance();
dataSync.purgeEventCodes().catch(err => {
console.error("Error purging event codes:", err);
});
}, []);
async function handleEventCheckIn(eventCode: string): Promise<void> {
try {
const get = Get.getInstance();
const auth = Authentication.getInstance();
const dataSync = DataSyncService.getInstance();
const logger = SendLog.getInstance();
const currentUser = auth.getCurrentUser();
if (!currentUser) {
await logger.send(
"error",
"event_check_in",
"Check-in failed: User not logged in"
);
toast.error("You must be logged in to check in to events");
return;
}
// Log the check-in attempt
await logger.send(
"info",
"event_check_in",
`Attempting to check in with code: ${eventCode}`
);
// Validate event code
if (!eventCode || eventCode.trim() === "") {
await logger.send(
"error",
"event_check_in",
"Check-in failed: Empty event code"
);
toast.error("Please enter an event code");
return;
}
// Get event by code
const events = await get.getList<Event>(
Collections.EVENTS,
1,
1,
`event_code="${eventCode}"`
);
if (events.totalItems === 0) {
await logger.send(
"error",
"event_check_in",
`Check-in failed: Invalid event code: ${eventCode}`
);
toast.error("Invalid event code. Please try again.");
return;
}
const event = events.items[0];
// Check if event is published
if (!event.published) {
await logger.send(
"error",
"event_check_in",
`Check-in failed: Event not published: ${event.event_name}`
);
toast.error("This event is not currently available for check-in");
return;
}
// Check if the event is active (has started and hasn't ended yet)
const currentTime = new Date();
const eventStartDate = new Date(event.start_date);
const eventEndDate = new Date(event.end_date);
if (currentTime < eventStartDate) {
await logger.send(
"error",
"event_check_in",
`Check-in failed: Event has not started yet: ${event.event_name}`
);
toast.error(`This event hasn't started yet. It begins on ${eventStartDate.toLocaleDateString()} at ${eventStartDate.toLocaleTimeString()}`);
return;
}
if (currentTime > eventEndDate) {
await logger.send(
"error",
"event_check_in",
`Check-in failed: Event has already ended: ${event.event_name}`
);
toast.error("This event has already ended");
return;
}
// Check if user is already checked in - IMPROVED VALIDATION
const attendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
50, // Increased limit to ensure we catch all possible duplicates
`user="${currentUser.id}" && event="${event.id}"`
);
if (attendees.totalItems > 0) {
const lastCheckIn = new Date(attendees.items[0].time_checked_in);
const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime();
const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60));
await logger.send(
"error",
"event_check_in",
`Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)`
);
toast.error(`You have already checked in to this event (${hoursAgo} hours ago)`);
return;
}
// Set current event for check-in
setCurrentCheckInEvent(event);
// Log successful event lookup
await logger.send(
"info",
"event_check_in",
`Found event for check-in: ${event.event_name}`
);
// Store event code in local storage for offline check-in
try {
await dataSync.storeEventCode(eventCode);
} catch (syncError) {
// Log the error but don't show a toast to the user
console.error("Error storing event code locally:", syncError);
}
// Show event details toast only for non-food events
// For food events, we'll show the toast after food selection
if (!event.has_food) {
toast.success(
<div>
<strong>Event found!</strong>
<p className="text-sm mt-1">{event.event_name}</p>
<p className="text-xs mt-1">
{event.points_to_reward > 0 ? `${event.points_to_reward} points` : "No points"}
</p>
</div>,
{ duration: 5000 }
);
}
// If event has food, show food selection modal
if (event.has_food) {
// Show food-specific toast
toast.success(
<div>
<strong>Event with food found!</strong>
<p className="text-sm mt-1">{event.event_name}</p>
<p className="text-xs mt-1">Please select the food you ate (or will eat) at the event!</p>
</div>,
{ duration: 5000 }
);
const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
if (modal) modal.showModal();
} else {
// If no food, show confirmation modal
const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
if (modal) modal.showModal();
}
} catch (error: any) {
console.error("Error checking in:", error);
toast.error(error.message || "An error occurred during check-in");
}
}
async function completeCheckIn(event: Event, foodSelection: string | null): Promise<void> {
try {
setIsLoading(true);
const auth = Authentication.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
const dataSync = DataSyncService.getInstance();
const get = Get.getInstance();
const currentUser = auth.getCurrentUser();
if (!currentUser) {
throw new Error("You must be logged in to check in to events");
}
const userId = currentUser.id;
const eventId = event.id;
// Double-check for existing check-ins with improved validation
const existingAttendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
50, // Increased limit to ensure we catch all possible duplicates
`user="${userId}" && event="${eventId}"`
);
if (existingAttendees.totalItems > 0) {
const lastCheckIn = new Date(existingAttendees.items[0].time_checked_in);
const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime();
const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60));
await logger.send(
"error",
"event_check_in",
`Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)`
);
throw new Error(`You have already checked in to this event (${hoursAgo} hours ago)`);
}
// Create new attendee record with transaction to prevent race conditions
const attendeeData = {
user: userId,
event: eventId,
food_ate: foodSelection || "",
time_checked_in: new Date().toISOString(),
points_earned: event.points_to_reward || 0
};
try {
// Create the attendee record in PocketBase
const newAttendee = await update.create(Collections.EVENT_ATTENDEES, attendeeData);
// Update user's total points
// First, get all the user's attendance records to calculate total points
const userAttendance = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1000,
`user="${userId}"`
);
// Calculate total points
let totalPoints = 0;
userAttendance.items.forEach(attendee => {
totalPoints += attendee.points_earned || 0;
});
// Update the LimitedUser record with the new points total
try {
// Try to get the LimitedUser record to check if it exists
let limitedUserExists = false;
try {
const limitedUser = await get.getOne(Collections.LIMITED_USERS, userId);
limitedUserExists = !!limitedUser;
} catch (e) {
// Record doesn't exist
limitedUserExists = false;
}
// Create or update the LimitedUser record
if (limitedUserExists) {
await update.updateFields(Collections.LIMITED_USERS, userId, {
points: JSON.stringify(totalPoints),
total_events_attended: JSON.stringify(userAttendance.totalItems)
});
} else {
// Get user data to create LimitedUser record
const userData = await get.getOne(Collections.USERS, userId);
if (userData) {
await update.create(Collections.LIMITED_USERS, {
id: userId, // Use same ID as user record
name: userData.name || 'Anonymous User',
major: userData.major || '',
points: JSON.stringify(totalPoints),
total_events_attended: JSON.stringify(userAttendance.totalItems)
});
}
}
} catch (error) {
console.error('Failed to update LimitedUser record:', error);
}
// Ensure local data is in sync with backend
// First sync the new attendance record
try {
await dataSync.syncCollection(Collections.EVENT_ATTENDEES);
// Then sync the updated user and LimitedUser data
await dataSync.syncCollection(Collections.USERS);
await dataSync.syncCollection(Collections.LIMITED_USERS);
} catch (syncError) {
// Log the error but don't show a toast to the user
console.error('Local sync failed:', syncError);
}
// Clear event code from local storage
try {
await dataSync.clearEventCode();
} catch (clearError) {
// Log the error but don't show a toast to the user
console.error("Error clearing event code from local storage:", clearError);
}
// Log successful check-in
await logger.send(
"info",
"event_check_in",
`Successfully checked in to event: ${event.event_name}`
);
// Show success message with event name and points
const pointsMessage = event.points_to_reward > 0
? ` (+${event.points_to_reward} points!)`
: "";
toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`);
// Close any open modals
const foodModal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
if (foodModal) foodModal.close();
const confirmModal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
if (confirmModal) confirmModal.close();
setCurrentCheckInEvent(null);
setFoodInput("");
} catch (createError: any) {
console.error("Error creating attendance record:", createError);
// Check if this is a duplicate record error (race condition handling)
if (createError.status === 400 && createError.data?.data?.user?.code === "validation_not_unique") {
throw new Error("You have already checked in to this event");
}
throw createError;
}
} catch (error: any) {
console.error("Error completing check-in:", error);
toast.error(error.message || "An error occurred during check-in");
} finally {
setIsLoading(false);
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!currentCheckInEvent) return;
try {
const auth = Authentication.getInstance();
const logger = SendLog.getInstance();
const get = Get.getInstance();
const currentUser = auth.getCurrentUser();
if (!currentUser) {
throw new Error("You must be logged in to check in to events");
}
// Additional check to prevent duplicate check-ins right before submission
const existingAttendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1,
`user="${currentUser.id}" && event="${currentCheckInEvent.id}"`
);
// Check if user is already checked in
if (existingAttendees.totalItems > 0) {
throw new Error("You have already checked in to this event");
}
// Complete check-in with food selection
await completeCheckIn(currentCheckInEvent, foodInput);
} catch (error: any) {
console.error("Error submitting check-in:", error);
toast.error(error.message || "An error occurred during check-in");
}
};
return (
<>
<div className="card bg-base-100 shadow-xl border border-base-200 h-full">
<div className="card-body p-4 sm:p-6">
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Event Check-in</h3>
<div className="form-control w-full">
<label className="label">
<span className="label-text text-sm sm:text-base">Enter event code to check in</span>
</label>
<form onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.querySelector('input') as HTMLInputElement;
if (input.value.trim()) {
setIsLoading(true);
handleEventCheckIn(input.value.trim()).finally(() => {
setIsLoading(false);
input.value = "";
});
} else {
toast("Please enter an event code", {
icon: '⚠️',
style: {
borderRadius: '10px',
background: '#FFC107',
color: '#000',
},
});
}
}}>
<div className="flex flex-col sm:flex-row gap-2">
<input
type="password"
placeholder="Enter code"
className="input input-bordered flex-1 text-sm sm:text-base h-10 min-h-[2.5rem] w-full"
onKeyPress={(e) => {
if (e.key === "Enter") {
e.preventDefault();
}
}}
/>
<button
type="submit"
className="btn btn-primary h-10 min-h-[2.5rem] text-sm sm:text-base w-full sm:w-auto"
disabled={isLoading}
>
{isLoading ? (
<Icon
icon="line-md:loading-twotone-loop"
className="w-5 h-5"
inline={true}
/>
) : (
"Check In"
)}
</button>
</div>
</form>
</div>
</div>
</div>
{/* Food Selection Modal */}
<dialog id="foodSelectionModal" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg mb-2">{currentCheckInEvent?.event_name}</h3>
<div className="text-sm mb-4 opacity-75">
{currentCheckInEvent?.event_description}
</div>
<div className="badge badge-primary mb-4">
{currentCheckInEvent?.points_to_reward} points
</div>
<p className="mb-4">This event has food! Please let us know what you ate (or will eat):</p>
<form onSubmit={handleSubmit}>
<div className="form-control">
<input
type="text"
placeholder="Enter the food you will or are eating"
className="input input-bordered w-full"
value={foodInput}
onChange={(e) => setFoodInput(e.target.value)}
required
/>
</div>
<div className="modal-action">
<button type="button" className="btn" onClick={() => {
const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
modal.close();
setCurrentCheckInEvent(null);
}}>Cancel</button>
<button type="submit" className="btn btn-primary" disabled={isLoading}>
{isLoading ? (
<Icon
icon="line-md:loading-twotone-loop"
className="w-5 h-5"
inline={true}
/>
) : (
"Check In"
)}
</button>
</div>
</form>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
{/* Confirmation Modal (for events without food) */}
<dialog id="confirmCheckInModal" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg mb-2">{currentCheckInEvent?.event_name}</h3>
<div className="text-sm mb-4 opacity-75">
{currentCheckInEvent?.event_description}
</div>
<div className="badge badge-primary mb-4">
{currentCheckInEvent?.points_to_reward} points
</div>
<p className="mb-4">Are you sure you want to check in to this event?</p>
<div className="modal-action">
<button
type="button"
className="btn"
onClick={() => {
const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
modal.close();
setCurrentCheckInEvent(null);
}}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
disabled={isLoading}
onClick={() => {
if (currentCheckInEvent) {
completeCheckIn(currentCheckInEvent, null);
const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
modal.close();
}
}}
>
{isLoading ? (
<Icon
icon="line-md:loading-twotone-loop"
className="w-5 h-5"
inline={true}
/>
) : (
"Confirm Check In"
)}
</button>
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
</>
);
};
export default EventCheckIn;

Some files were not shown because too many files have changed in this diff Show more