# Project export: UCSC Club Finder

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: UCSC Club Finder sorts campus clubs to match your interests. After a short questionnaire, the Chrome extension returns relevant clubs with contacts and brief descriptions.
- Devpost: https://devpost.com/software/ucsc-club-finder
- GitHub: https://github.com/BasiCubes950/cruzhack-yippie
- Video: https://www.youtube.com/embed/KjZ7mLK1JyM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Basicubes (15 commits), Lam1430 (6 commits), BasiCubes950 (1 commits)

## Devpost submission (written by the team)

### Inspiration

Our inspiration for this project came from a quote by a UCSC student: “My regret is not being more involved in campus activities, clubs, etc. Do well in your classes, but also let yourself have fun and try new things.” One major barrier to an engaging and fulfilling college experience is the lack of involvement a student might have in clubs. This often stems from the difficulty of finding organizations that genuinely align with a student’s interests and finding the correct club from a sea of information. UCSC Activity Finder was created to address this problem. What It Does UCSC Activity Finder provides users with a short questionnaire that identifies their interests and matches them with relevant campus clubs. Based on the quiz results, the extension generates a curated list of clubs along with their contact information and descriptions. Users can explore this refined list to find activities that suit them or “roll the dice” to discover a random club to join. By centralizing all campus clubs in one place and narrowing them into manageable lists, the extension makes student involvement more accessible and less overwhelming. How We Built It We initially used Python to prototype and test the core logic of the system. Once the main functions were working, we transitioned the codebase to JavaScript and HTML for the Chrome extension. Club data and associated tags were stored in a JSON file, which was generated beforehand using Python. HTML was used to structure the extension’s layout, while JavaScript handled the application logic and interactivity. We also used Canva to design the visual elements and overall layout of the extension. Challenges We Ran Into One of the main challenges was determining which questions to ask and how to structure them to minimize time spent on answering. We spent a significant amount of time refining the questionnaire to accurately capture user interests while keeping it concise. The biggest challenge, however, was formatting the extension itself. Aligning buttons, text, and overflow behavior with our Canva mock-ups proved difficult, and resolving these layout issues took the better part of Saturday. Accomplishments That We’re Proud Of This was our team’s first hackathon, and we were proud to complete a functional project from start to finish. As high school students, it was especially fun to compete alongside college students. Another major accomplishment was having our base product completed by the end of the Friday. What We Learned Through this project, we learned how sensitive HTML layouts can be when combined with JavaScript behavior. We also gained valuable experience in organizing, storing, and efficiently searching through large datasets. What’s Next for UCSC Activity Finder We will focus on perfecting our extension. Once we have done some testing with UCSC students and improved our searching algorithm, we will expand to other colleges. We can also improve user experience.

## README (from the GitHub repository)

# cruzhack-yippie
we are hacking

Instructions:
1. clone repository
2. click on extensions in top right menu of chrome
3. click manage extensions
4. select "Load unpack"
5. set the folder to "club-extensions" from the cloned repository
6. you is done! yippie!


## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 30 KB.
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (11 of 11)

```
activities.json
club-extension/activities.json
club-extension/manifest.json
club-extension/popup.html
club-extension/popup.js
data.py
main.html
quiz.py
README.md
searching.py
test.py
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Update README with extension loading instructions
- hide the problems
- prefinal
- uvlight
- dhasdasjhdasjhdas
- mostnew
- newone
- SAVE
- back
- Merge branch 'main' of https://github.com/BasiCubes950/cruzhack-yippie into UI
- hide the start over button
- move start over
- updating branch
- added homepage
- test
- got quiz working
- code
- working description on extension
- update
- profiling

## Key source files (fetched from GitHub, selected and truncated for size)

### main.html

```html
<html>
  <head>
  </head>
  <body>
  </body>
</html>
```

### test.py

```python
import json

def load_activities(json_file="activities.json"):
    with open(json_file, "r", encoding="utf-8") as f:
        return json.load(f)


def search_clubs(activities, tags, user_gender):
    """
    activities: list of dicts loaded from activities.json
    tags: iterable of tags to search
    user_gender: "***", "**", or "*"

    returns: list of (name, contact, link)
    """

    results = []
    seen = set()

    for activity in activities:
        for tag in tags:
            if tag not in activity:
                continue

            for club in activity[tag]:
                club_name = club[0]
                contact = club[1]   # Instagram
                link = club[1]

                # gender check ONLY if block exists
                if len(club) >= 3 and club[2] != user_gender:
                    continue

                key = (club_name, contact)
                if key not in seen:
                    seen.add(key)
                    results.append((club_name, contact, link))

    return results

"""
* -> other
** -> women
*** ->  men
"""

user_gender = "**"
tags = {"volunteering", "community"}

activities = load_activities("activities.json")
matches = search_clubs(activities, tags, user_gender)

for name, contact, link in matches:
    print(name, contact, link)

```

### quiz.py

```python
def run_profile_quiz():
    profile = {}

    # --- Basic profiling ---
    print("Basic Profiling\n")

    profile["gender"] = input(
        "Gender (Male / Female / LGBTQ+ / Prefer not to say): "
    ).strip()

    profile["ethnicity"] = input(
        "Ethnicity (Optional / Prefer not to say): "
    ).strip()

    # --- Interest sorting (1–5 scale) ---
    print("\nInterest Sorting (1 = Not at all, 5 = Very much)\n")

    def scale_question(prompt):
        while True:
            try:
                value = int(input(prompt + " (1–5): "))
                if 1 <= value <= 5:
                    return value
            except ValueError:
                pass
            print("Please enter a number from 1 to 5.")

    interests = {}
    interests["programming"] = scale_question(
        "How much do you enjoy programming?"
    )
    interests["robotics"] = scale_question(
        "How interested are you in robotics / hardware?"
    )
    interests["ai"] = scale_question(
        "How interested are you in AI / data / algorithms?"
    )
    interests["problem_solving"] = scale_question(
        "How much do you enjoy problem-solving challenges?"
    )

    profile["interests"] = interests

    return profile

user_profile = run_profile_quiz()
print("\nStored profile:")
print(user_profile)
```

### searching.py

```python
import json

def load_activities(json_file="activities.json"):
    with open(json_file, "r", encoding="utf-8") as f:
        return json.load(f)

def search_clubs(activities, tags, user_gender):
    """
    activities: list of dicts loaded from activities.json
    tags: iterable of tags to search (e.g., {"volunteering", "STEM"})
    user_gender: "***" (men), "**" (women), or "*" (other)

    returns: list of dictionaries for better readability
    """
    results = []
    seen = set()

    for activity_dict in activities:
        for tag in tags:
            # Check if this category exists in the current dictionary
            if tag not in activity_dict:
                continue

            for club in activity_dict[tag]:
                # Mapping based on the new list structure:
                # [0]: Name, [1]: Instagram, [2]: Description, [3]: Email, [4]: Gender
                name = club[0]
                instagram = club[1]
                description = club[2]
                email = club[3]
                
                # Check for gender block (it will be at index 4 if it exists)
                # If club has a gender requirement and it doesn't match the user, skip.
                if len(club) > 4 and club[4] != user_gender:
                    continue

                # Use name as the unique key to avoid duplicates across multiple tags
                if name not in seen:
                    seen.add(name)
                    results.append({
                        "name": name,
                        "instagram": instagram,
                        "description": description,
                        "email": email
                    })

    return results

# --- Example Usage ---

# Mapping Reference:
# * -> other
# ** -> women
# *** -> men

user_gender = "**"
search_tags = {"volunteering", "STEM"}

activities_data = load_activities("activities.json")
matches = search_clubs(activities_data, search_tags, user_gender)

print(f"Found {len(matches)} matches:\n")
for club in matches:
    print(f"Club: {club['name']}")
    print(f"Contact: {club['email']} | {club['instagram']}")
    print(f"Description: {club['description']}") # Print first 100 chars
    print("-" * 30)
```

### data.py

```python
import pandas as pd
import json
from collections import defaultdict

def build_activities_from_sheet(file_path, output_json="activities.json"):
    # Load data
    if file_path.endswith('.csv'):
        # using utf-8-sig to handle BOM if present, otherwise utf-8
        df = pd.read_csv(file_path, encoding='utf-8')
    else:
        df = pd.read_excel(file_path)

    activities_map = defaultdict(list)

    # 1. Config: Gender Mapping
    GENDER_MAP = {
        "women": "**",
        "men": "***",
        "other": "*"
    }

    # 2. Config: Columns to exclude from being treated as Tags
    NON_TAG_COLS = {"Club", "Instagram", "Email", "Original Description"}

    # Dynamically identify tag columns
    tag_columns = [c for c in df.columns if c not in NON_TAG_COLS]

    for _, row in df.iterrows():
        # --- Extract Basic Info ---
        club_name = str(row["Club"]).strip()
        
        # Instagram
        raw_link = row.get("Instagram")
        link = str(raw_link).strip() if pd.notna(raw_link) else ""

        # Description
        raw_desc = row.get("Original Description")
        description = str(raw_desc).strip() if pd.notna(raw_desc) else ""

        # Contact / Email
        raw_email = row.get("Email")
        email = str(raw_email).strip() if pd.notna(raw_email) else ""

        # --- Detect Gender Block ---
        gender_block = None
        for gender_col, symbol in GENDER_MAP.items():
            if gender_col in df.columns:
                val = str(row.get(gender_col)).strip().lower()
                if val == "x":
                    gender_block = symbol
                    break

        # --- Build Tag Lists ---
        for tag in tag_columns:
            # Skip gender columns (they are attributes, not categories)
            if tag in GENDER_MAP:
                continue

            # Check if this club belongs to this tag
            if str(row.get(tag)).strip().lower() == "x":
                
                # Construct the entry
                # Order: [Name, Instagram, Description, Email, (Gender)]
                entry = [club_name, link, description, email]
                
                if gender_block:
                    entry.append(gender_block)

                activities_map[tag].append(entry)

    # Convert to list-of-dicts format
    activities = [{tag: clubs} for tag, clubs in activities_map.items()]

    # Save to JSON
    with open(output_json, "w", encoding="utf-8") as f:
        json.dump(activities, f, indent=2, ensure_ascii=False)

    print(f"Processed {len(activities)} categories successfully.")
    return activities

# Usage
activities = build_activities_from_sheet("dirty table.xlsx")
```

### club-extension/popup.html

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>UCSC Activity Finder</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      width: 360px;
      height: 520px;
      padding: 10px;
      box-sizing: border-box;

      background-image: url("Homepage.png");
      background-size: cover;
      background-repeat: no-repeat;
      background-position: center;
      background-attachment: fixed;   /* ✅ background stays still */
      overflow: hidden;              /* ✅ body never scrolls */
    }

    /* Wrapper fills the popup */
    #all {
      height: 100%;
      display: flex;
      flex-direction: column;
    }

    /* ---------- SHARED ---------- */
    button {
      width: 100%;
      padding: 10px;
      font-size: 14px;
      cursor: pointer;
      border-radius: 12px;
      border-style: hidden;
      font-weight: bold;
    }

    .hidden { display: none; }

    .small {
      font-size: 12px;
      color: #555;
    }

    /* ---------- HOME PAGE ---------- */
    #home {
      text-align: center;
      margin-top: 110%;
    }

    #startBtn {
      margin: 0 auto;
      color: white;
      background-color: #182b49;
      border-style: hidden;
      margin-bottom: 30px;
    }

    /* Yellow overlay to hide buttons on homepage */
    #homeOverlay {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      height: 40px;
      background-color: #c69214;
      z-index: 10;
      pointer-events: none;
    }

    #homeOverlay.hidden {
      display: none;
    }

    /* ---------- QUIZ + RESULTS CONTAINER ---------- */
    #app {
      /* Takes full available popup space */
      flex: 1;
      display: flex;
      flex-direction: column;
      min-height: 0; /* ✅ allows children to scroll properly in flex layouts */
    }

    /* ---------- QUIZ ---------- */
    .q { margin-top: 12px; }

    .q-title {
      font-weight: bold;
      margin-bottom: 60px;
      height: 100px;
      font-size: 30px;
      display: block;
      color: #ffcd00;
    }

    .opt {
      display: block;
      padding: 14px;
      margin: 8px 0;
      background: #ffffff;
      border-style: hidden;
      border-radius: 12px;
      cursor: pointer;
      text-align: center;
      font-size: 13px;
      transition: all 0.2s ease;
      user-select: none;
      font-weight: bold;
    }

    .opt:hover {
      background: #f0f0f0;
      border-style: hidden;
    }

    .opt.selected {
      background: #182b49;
      color: white;
      border-color: #182b49;
    }

    .opt.selected:hover { background: #0f1e36; }

    /* ---------- RESULTS ---------- */
    /* ✅ Only this section scrolls, so background stays still */
    #results {
      flex: 1;               /* fill remaining space */
      min-height: 0;         /* ✅ required so overflow works in flex */
      overflow-y: auto;      /* ✅ scrollable */
      padding-right: 6px;    /* avoid scrollbar overlap */
      margin-top: 8px;
    }

    .club {
      margin-top: 12px;
      padding-top: 8px;
      border-top: 1px solid #ddd;
      font-size: 13px;
    }

    .toggleRow {
      margin-top: 6px;
      display: flex;
      gap: 6px;
    }

    .toggleRow button {
      width: auto;
      flex: 1;
      padding: 6px;
    }

    .desc {
      margin-top: 6px;
      padding: 6px;
      background: #f6f6f6;
      border-radius: 6px;
    }

    /* ---------- NAV ---------- */
    #navRow {
      display: flex;
      gap: 8px;
      margin-top: 10px;
    }

    #resetBtn {
      margin-top: 12px;
      margin-bottom: 12px;
    }
  </style>
</head>

<body>
  <div id="all">
    <!-- HOME PAGE -->
    <div id="home">
      <button id="startBtn">EXPLORE</button>
      <button id="luckyBtn">I'M FEELING LUCKY</button>
    </div>

    <!-- Yellow overlay to hide Back/Next buttons on homepage -->
    <div id="homeOverlay"></div>

    <!-- QUIZ + RESULTS -->
    <div id="app" class="hidden">
      <div id="quiz"></div>

      <div id="navRow">
        <button id="backBtn">Back</button>
        <button id="nextBtn">Next</button>
      </div>

      <button id="resetBtn" class="hidden">Start Over</button>

      <div id="status" class="small"></div>

      <!-- ✅ This is the only scrollable container -->
      <div id="results"></div>
    </div>
  </div>

  <script src="popup.js"></script>
</body>
</html>

```

### club-extension/popup.js

```javascript
// popup.js
// Branching quiz wizard + local activities.json search (no server)

const homeDiv = document.getElementById("home");
const appDiv = document.getElementById("app");

document.getElementById("startBtn").addEventListener("click", () => {
  homeDiv.classList.add("hidden");
  appDiv.classList.remove("hidden");
  document.getElementById("homeOverlay").classList.add("hidden");
  document.body.style.backgroundImage = "url('Quiz.png')";
  renderStep();
});

document.getElementById("luckyBtn").addEventListener("click", async () => {
  homeDiv.classList.add("hidden");
  appDiv.classList.remove("hidden");
  document.body.style.backgroundImage = "url('Results.png')";
  document.getElementById("nextBtn").style.marginTop = "60px";
  document.getElementById("backBtn").style.marginTop = "60px";
  await showRandomClub();
});


// -------------------- DATA LOAD --------------------
async function loadActivities() {
  const url = chrome.runtime.getURL("activities.json");
  const response = await fetch(url);
  return await response.json();
}

// -------------------- SEARCH --------------------
function searchClubs(activities, tags, userGender) {
  const results = [];
  const seen = new Set();
  const GENDER_BLOCKS = new Set(["*", "**", "***"]);

  for (const activity of activities) {
    for (const tag of tags) {
      if (!(tag in activity)) continue;

      for (const club of activity[tag]) {
        const name = club[0];
        const contact = club[1];

        // gender filtering ONLY if a gender block exists
        const lastField = club[club.length - 1];
        if (GENDER_BLOCKS.has(lastField) && lastField !== userGender) continue;

        // common layout: [name, instagram, description, email, (optional gender)]
        const description =
          typeof club[2] === "string" && club[2].trim() ? club[2] : "(No description)";
        const email =
          typeof club[3] === "string" && club[3].trim() ? club[3] : "";

        const key = `${name}|${contact}`;
        if (!seen.has(key)) {
          seen.add(key);
          results.push({ name, contact, email, description });
        }
      }
    }
  }

  return results;
}

// -------------------- RESULT UI (SHOW/HIDE DESCRIPTION) --------------------
function renderResults(matches, resultsDiv) {
  resultsDiv.innerHTML = "";

  if (matches.length === 0) {
    resultsDiv.innerHTML = `<div class="small">No clubs found for those tags.</div>`;
    return;
  }

  for (const club of matches) {
    const wrapper = document.createElement("div");
    wrapper.className = "club";

    const header = document.createElement("div");
    header.innerHTML = `
      <b>${club.name}</b><br/>
      <span class="small">${club.contact}${club.email ? " • " + club.email : ""}</span>
    `;

    const btnRow = document.createElement("div");
    btnRow.className = "toggleRow";

    const showBtn = document.createElement("button");
    showBtn.textContent = "Show description";

    const hideBtn = document.createElement("button");
    hideBtn.textContent = "Hide description";
    hideBtn.disabled = true;

    const descBox = document.createElement("div");
    descBox.className = "desc hidden";
    descBox.textContent = club.description;

    showBtn.addEventListener("click", () => {
      descBox.classList.remove("hidden");
      showBtn.disabled = true;
      hideBtn.disabled = false;
    });

    hideBtn.addEventListener("click", () => {
      descBox.classList.add("hidden");
      showBtn.disabled = false;
      hideBtn.disabled = true;
    });

    btnRow.appendChild(showBtn);
    btnRow.appendChild(hideBtn);

    wrapper.appendChild(header);
    wrapper.appendChild(btnRow);
    wrapper.appendChild(descBox);

    resultsDiv.appendChild(wrapper);
  }
}

// -------------------- GENDER (FOR FILTERING) --------------------
function genderAnswerToBlock(answer) {
  // * -> other, ** -> women, *** -> men
  if (answer === "Male") return "***";
  if (answer === "Female") return "**";
  return "*"; // LGBTQ+ or Prefer not to share
}

// -------------------- LEAF -> TAG MAPPING --------------------
const LEAF_TAGS = {
  // PURPOSE / IMPACT
  "#11": ["volunteering"],
  "#10": ["advocacy / civic"],

  // SKILLS / GROWTH
  "#12": ["STEM", "computing", "research", "mentorship"],
  "#13": ["skill-building", "media / publication", "design"],

  // HOBBIES
  "#5": ["music"],
  "#7": ["performing-arts"],
  "#6": ["gaming"],
  "#9": ["design", "media / publication"],
  "#8": ["culture", "gaming"],

  // IDENTITIES
  "#1": ["identity", "culture"],
  "#3": ["religion / spirituality"],
  "#2": ["identity", "identity-support"],
  "#4": ["identity-support"]
};

// -------------------- WIZARD STATE --------------------
const appState = {
  userGender: null,
  tags: new Set(),
  // history stack for "Back"
  history: [], // {stepId, selectedValue}
  stepId: "gender_profile"
};

// -------------------- QUIZ STEPS (BRANCHING) --------------------
const STEPS = {
  gender_profile: {
    title: "Profiling: How do you define yourself?",
    type: "radio",
    options: [
      { label: "Male", value: "Male", next: "q1" },
      { label: "Female", value: "Female", next: "q1" },
      { label: "LGBTQ+", value: "LGBTQ+", next: "q1" },
      { label: "I don’t want to share", value: "I don’t want to share", next: "q1" }
    ],
    onSelect: (value) => {
      appState.userGender = genderAnswerToBlock(value);
    }
  },

  q1: {
    title: "Which of these sounds best right now?",
    type: "radio",
    options: [
      { label: "Meeting people with similar interest", value: "similar", next: "q_meet_similar" },
      { label: "Something purposeful or growth-focused", value: "purpose", next: "q_purpose_growth" },
      { label: "Not sure", value: "not_sure", next: "q_hobbies" }
    ]
  },

  // Purpose/growth branch
  q_purpose_growth: {
    title: "What kind of purpose or growth?",
    type: "radio",
    options: [
      { label: "Helping others / social impact", value: "impact", next: "q_meani
[truncated — 12071 more characters]
```