# Project export: Planify - Google Extension

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: Cal Hacks 12.0
- Tagline: Planify is a Chrome extension that helps you turn any webpage or email into a calendar event. It checks your Google Calendar for conflicts and lets you add or edit events without switching tabs.
- Devpost: https://devpost.com/software/planify-google-extension
- GitHub: https://github.com/hieuduong61/calhack25
- Video: https://www.youtube.com/embed/OVM-YJzcJsc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — ryanhongnguyen (21 commits), Hieu Duong (10 commits)

## Devpost submission (written by the team)

### Inspiration

Planify came from a small frustration I kept having. I’d often get event invites through emails or websites and forget to add them to my calendar. It felt repetitive to copy all the details, switch tabs, and check my schedule every time. I wanted to build something that could make that process easier and faster.

### What it does

Planify is a Chrome extension that automatically detects event information from any webpage or email. It checks your Google Calendar to show whether you’re available and lets you add or edit events right there, without switching tabs. It’s meant to help people stay organized while browsing without interrupting their flow.

### How we built it

We built Planify using a Chrome extension frontend and a Node.js/Express backend. The extension sends webpage content to the backend, which uses Anthropic’s API to extract event details with natural language processing. Then it connects to the Google Calendar API to check for conflicts and availability. We implemented OAuth2 so users can log in securely with their Google account.

### Challenges we ran into

Getting Google’s OAuth flow to work inside a Chrome extension was one of the hardest parts. We also struggled with parsing inconsistent event formats from different websites. Debugging CORS issues and keeping the extension lightweight while handling all these features took a lot of testing and iteration.

### Accomplishments we're proud of

We’re proud that the extension works smoothly across different websites and integrates seamlessly with Google Calendar. Seeing event details automatically appear and being able to add them with one click felt like a big win. It was also satisfying to connect AI, backend logic, and frontend UI all in one project.

### What we learned

We learned a lot about how Chrome extensions are structured and how different parts communicate with each other. Implementing OAuth2 and handling API calls securely taught us a lot about authentication. We also got hands-on experience using AI models to extract useful information from messy real-world text.

### What's next

for Planify Next, we want to make Planify not just detect events but also suggest what to do during free time slots in the calendar. For example, it could recommend nearby events, study sessions, or personal tasks based on your interests and schedule. The goal is to turn Planify into a smarter assistant that helps you make better use of your time, not just manage it.

## README (from the GitHub repository)

# 📅 Planify - Smart Event Calendar Extension

**Transform any webpage into calendar events with AI - no tab switching required**

Planify instantly extracts event information from any webpage and shows you if you're available at that time slot - all without leaving your current page.

## 🎯 **The Problem It Solves**

**Without Planify:** You receive event invitations via email/websites and need to:
1. Open each email/website
2. Manually note event details
3. Switch to Google Calendar
4. Check availability
5. Create new events if attending

**With Planify:** Simply:
1. Click the extension icon on any page with event information
2. Instantly see your availability and conflicts
3. Click "Add" to save events you want to attend
4. Continue browsing - no tab switching needed!

## ✨ **Key Features**

- 🤖 **AI-Powered Extraction**: Automatically identifies events from any webpage
- ⚡ **Instant Availability Check**: Shows conflicts and nearby events immediately
- 🎯 **One-Click Adding**: Add events to your calendar without leaving the page
- ✏️ **Smart Editing**: Modify event details before adding
- 🔄 **Conflict Detection**: Red warnings for conflicts, yellow for nearby events
- 📧 **Email Integration**: Works perfectly with Gmail and other email clients
- 👤 **Multi-Account Support**: Switch between different Google accounts easily

## 🎬 **Video Demo**

*Coming soon - we'll add a demo video showing Planify in action!*

## 🛠️ **Setup Instructions**

### **Prerequisites**
- Google Cloud Platform account
- Anthropic account

### **Step 1: Google Cloud Setup**

1. **Create a Google Cloud Project:**
   - Go to [Google Cloud Console](https://console.cloud.google.com/)
   - Create a new project named "Planify Extension"

2. **Enable Google Calendar API:**
   - Go to "APIs & Services" > "Library"
   - Search for "Google Calendar API" and enable it

3. **Create OAuth2 Credentials:**
   - Go to "APIs & Services" > "Credentials"
   - Click "Create Credentials" > "OAuth 2.0 Client IDs"
   - Choose "Web application"
   - Add authorized redirect URI: `http://localhost:8080/auth/google/callback`
   - Download the credentials JSON file

4. **Configure OAuth Consent Screen:**
   - Go to "APIs & Services" > "OAuth consent screen"
   - Choose "External" user type
   - Fill in required fields (app name: "Planify", user support email, etc.)
   - Add scopes: `https://www.googleapis.com/auth/calendar` and `https://www.googleapis.com/auth/userinfo.email`

### **Step 2: Anthropic API Setup**

1. **Create Anthropic Account:**
   - Go to [Anthropic Console](https://console.anthropic.com/)
   - Sign up or log in

2. **Generate API Key:**
   - Navigate to "API Keys" section
   - Click "Create Key" and copy the generated key

### **Step 3: Local Development Setup**

1. **Install and Configure:**
   ```bash
   git clone <your-repo-url>
   cd planify-calendar-extension
   npm install
   ```

2. **Set up Environment:**
   - Copy `.env.example` to `.env`
   - Fill in your API keys:
   ```env
   ANTHROPIC_API_KEY=your_anthropic_key_here
   GOOGLE_CLIENT_ID=your_google_client_id_here
   GOOGLE_CLIENT_SECRET=your_google_client_secret_here
   GOOGLE_REDIRECT_URI=http://localhost:8080/auth/google/callback
   PORT=8080
   ```

3. **Start the Server:**
   ```bash
   docker build -t planify-server .
   ```

   ```bash
   docker run -p 8080:8080 planify-server
   ```

4. **Load Extension in Chrome:**
   - Open Chrome and go to `chrome://extensions/`
   - Enable "Developer mode" (toggle in top right)
   - Click "Load unpacked" and select the extension folder

### **Step 4: First-Time Authentication**

1. **Connect Google Calendar:**
   - Click the Planify extension icon
   - Click the user icon (👤) in the top right
   - Click "Use another account"
   - Complete Google OAuth flow in the popup window

2. **Test the Extension:**
   - Visit any webpage with event information (try a Gmail invitation)
   - Click the Planify extension icon
   - Click "Extract Events from Page"
   - You should see extracted events with availability information

## 📦 **Chrome Web Store Submission**

### **Preparing for Submission**

1. **Create Extension Package:**
   - Ensure all files are in the extension folder
   - Remove any development files (`.env`, `node_modules`, etc.)

2. **Create Store Assets:**
   - **Icons**: 16x16, 48x48, 128x128 PNG files (already included)
   - **Screenshots**: 1280x800 or 640x400 PNG files
   - **Promotional Images**: 440x280 PNG for featured placement

### **Submission Process**

1. **Developer Account:**
   - Go to [Chrome Web Store Developer Dashboard](https://chrome.google.com/webstore/devconsole/)
   - Pay the one-time $5 registration fee

2. **Upload Extension:**
   - Click "Add new item"
   - Upload your extension ZIP file
   - Fill in store listing details
   - Submit for review

3. **Review Process:**
   - Google typically reviews within 1-3 business days
   - Address any feedback or policy violations
   - Once approved, your extension goes live!

## 🔧 **Technical Details**

### **Architecture**
- **Frontend**: Chrome Extension (HTML/CSS/JavaScript)
- **Backend**: Node.js/Express server
- **AI**: Anthropic Claude for event extraction
- **Calendar**: Google Calendar API for scheduling

### **Security**
- All API calls go through your secure local backend
- Google OAuth2 handles authentication securely
- No sensitive data stored locally in the extension

## 🐛 **Troubleshooting**

**Events not extracting:**
- Make sure the backend server is running (`npm start`)
- Check that the server is accessible at `http://localhost:8080`

**Can't add to calendar:**
- Ensure Google Calendar is connected (click the user icon)
- Verify your Google OAuth2 credentials are correct

**Extension not loading:**
- Make sure all files are in the same folder
- Check Chrome's extension error messages

## 📄 **License**

MIT License - feel free to use, modify, and distribute!

---

**Ready to never miss another event? Install Planify and transform how you manage your calendar!** 🚀

## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 71 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (12 of 12)

```
.gitignore
background.js
content.js
Dockerfile
ex.html
manifest.json
package.json
popup.css
popup.html
popup.js
README.md
server.js
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.37.0, cors@^2.8.5, dotenv@^16.4.5, express@^5.1.0, googleapis@^164.1.0, jsdom@^27.0.1, node-fetch@^3.3.2

### Recent commits (newest first)

- add ping route
- update cloud url for frontend
- update cloud url
- update gitignore
- Dockerize server
- Merge branch 'main' of github.com:hieuduong61/calhack25
- edit the successful auth page
- change css file so everything is consistent
- Merge branch 'main' of github.com:hieuduong61/calhack25
- Merge branch 'main' of github.com:hieuduong61/calhack25
- chagne the button color for login again
- update successful auth page
- change the extract again button size
- change the button color
- change color button
- update readme and cleanup
- adding logo
- Merge branch 'main' of github.com:hieuduong61/calhack25
- make the authenticate page a popup, not new tab
- Merge pull request #4 from hieuduong61/ryan

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

### Dockerfile

```
# Use Node.js base image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install --production

# Copy the rest of your code
COPY . .

# Expose the port your Express server listens on (adjust if not 3000)
EXPOSE 8080

# Start the server
CMD ["node", "server.js"]

```

### package.json

```
{
  "name": "planify-calendar-extension",
  "version": "1.0.0",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "node server.js",
    "setup": "cp .env.example .env && echo 'Please edit .env file with your API keys'"
  },
  "keywords": [
    "chrome-extension",
    "google-calendar",
    "ai"
  ],
  "author": "",
  "license": "MIT",
  "description": "Chrome extension to extract events from webpages using AI and add to Google Calendar",
  "dependencies": {
    "@anthropic-ai/sdk": "^0.37.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^5.1.0",
    "googleapis": "^164.1.0",
    "jsdom": "^27.0.1",
    "node-fetch": "^3.3.2"
  }
}

```

### server.js

```javascript
import express from 'express';
import cors from 'cors';
import Anthropic from '@anthropic-ai/sdk';
import { google } from 'googleapis';
import dotenv from 'dotenv';
import { JSDOM } from 'jsdom';
import fs from "fs";
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Load environment variables
dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

app.use(cors());
app.use(express.json({ limit: '10mb' }));

// Serve logo image
app.get('/logo.png', (req, res) => {
  res.sendFile(path.join(__dirname, 'icons', 'logo.png'));
});

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

// Google OAuth2 configuration
const oauth2Client = new google.auth.OAuth2(
  process.env.GOOGLE_CLIENT_ID,
  process.env.GOOGLE_CLIENT_SECRET,
  process.env.GOOGLE_REDIRECT_URI || 'http://localhost:5000/auth/google/callback'
);

// Store user tokens and OAuth clients (in production, use a proper database)
const userTokens = new Map();
const userOAuthClients = new Map();

// Google OAuth routes
app.get('/auth/google', (req, res) => {
  // Generate a unique session ID for this user
  const sessionId = Math.random().toString(36).substring(2, 15);
  
  // Clear any existing sessions to ensure fresh authentication
  userTokens.clear();
  userOAuthClients.clear();
  
  const authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: ['https://www.googleapis.com/auth/calendar'],
    prompt: 'consent',
    state: sessionId // Pass session ID to identify the user
  });
  
  res.redirect(authUrl);
});

app.get('/auth/google/callback', async (req, res) => {
  try {
    const { code, state } = req.query;
    const { tokens } = await oauth2Client.getToken(code);
    
    // Store tokens with the session ID
    const sessionId = state || 'default';
    userTokens.set(sessionId, tokens);
    
    // Create a new OAuth client for this user
    const userOAuthClient = new google.auth.OAuth2(
      process.env.GOOGLE_CLIENT_ID,
      process.env.GOOGLE_CLIENT_SECRET,
      process.env.GOOGLE_REDIRECT_URI || 'http://localhost:5000/auth/google/callback'
    );
    userOAuthClient.setCredentials(tokens);
    userOAuthClients.set(sessionId, userOAuthClient);
    
    res.send(`
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Planify - Authentication Success</title>
        <style>
          * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
          }
          
          body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
            background: linear-gradient(135deg, #1a73e8 0%, #1557b0 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
          }
          
          .container {
            background: white;
            border-radius: 20px;
            padding: 60px 40px;
            text-align: center;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
            max-width: 500px;
            width: 100%;
          }
          
          .logo {
            margin-bottom: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
          }
          
          .logo-image {
            height: 60px;
            width: auto;
            animation: pulse 2s infinite;
          }
          
          .logo-text {
            font-size: 48px;
            color: #1a73e8;
            font-weight: 700;
          }
          
          @keyframes pulse {
            0%, 100% {
              transform: scale(1);
            }
            50% {
              transform: scale(1.1);
            }
          }
          
          .success-icon {
            font-size: 80px;
            margin-bottom: 30px;
            animation: bounce 2s infinite;
          }
          
          @keyframes bounce {
            0%, 20%, 50%, 80%, 100% {
              transform: translateY(0);
            }
            40% {
              transform: translateY(-8px);
            }
            60% {
              transform: translateY(-4px);
            }
          }
          
          h1 {
            color: #2d3748;
            font-size: 32px;
            font-weight: 700;
            margin-bottom: 20px;
          }
          
          .subtitle {
            color: #4a5568;
            font-size: 18px;
            margin-bottom: 40px;
            line-height: 1.6;
          }
          
          .cta-button {
            background: linear-gradient(135deg, #1a73e8 0%, #1557b0 100%);
            color: white;
            border: none;
            padding: 15px 30px;
            border-radius: 50px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s ease;
            text-decoration: none;
            display: inline-block;
          }
          
          .cta-button:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 20px rgba(26, 115, 232, 0.3);
          }
          
          .footer {
            margin-top: 30px;
            color: #718096;
            font-size: 14px;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
          }
          
          .footer-icon {
            font-size: 16px;
          }
        </style>
      </head>
      <body>
        <div class="container">
          <div class="logo">
            <img src="/logo.png" alt="Planify Logo" class="logo-image">
          </div>
          <div class="success-icon">🎉</div>
          <h1>Successfully Con
[truncated — 18697 more characters]
```

### content.js

```javascript
console.log('Event Extractor content script loaded');

```

### background.js

```javascript
chrome.runtime.onInstalled.addListener(() => {
  console.log('Planify extension installed');
});

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content.js']
  });
});

```

### ex.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Event Extractor</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
    Dear GLOBE Family, 

    You are invited to GLOBE's Friendsgiving event!
    
    On Tuesday November 18, 2025 from 2:00PM-4:00PM, we invite you all to the Seybor Room, which is located inside The Faculty Club to come and celebrate Friendsgiving together! We will have a buffet of food and drinks and we are excited to see you there! 
    
    If you are interested in attending, please submit this RSVP form below with your UC Berkeley email address.  
    Also there is another event on Thursday November 20, 2025 from 8:00PM-9:00PM, we invite you all to the Ryan Room, Hieu Hall. there will be a workshop for calhack 12 info session and we will have a few speakers from calhack 12 to talk about the project and the experience.
    There will also be a Birthday party for Emma's Birthday! at 2-3pm on Friday November 21, 2025 at Khue's apartment in Berkeley..
</body>
</html>

```

### popup.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Planify</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <div class="top-bar">
      <div class="top-left">
        <img src="./icons/logo.png" alt="Icon" style="width: 50%; height: 20%; ">
        <button id="re-extract-btn" style="width: 50%;" class="btn btn-primary btn-small hidden">Extract Again</button>
      </div>
      <div class="top-actions">
        <div class="user-dropdown">
          <button id="user-btn" class="btn-icon" title="User Account">
            <span>👤</span>
          </button>
          <div id="user-dropdown-menu" class="dropdown-menu hidden">
            <div class="dropdown-item">
              <div class="user-email" id="user-email">Not authenticated</div>
            </div>
            <div class="dropdown-divider"></div>
            <div class="dropdown-item">
              <button id="switch-account-btn" class="dropdown-link">Use another account</button>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div id="loading" class="loading hidden">
      <div class="spinner"></div>
      <p>Analyzing page content...</p>
    </div>

    <div id="error" class="error hidden">
      <p class="error-message"></p>
      <button id="retry-btn" class="btn btn-secondary">Try Again</button>
    </div>

    <div id="no-events" class="no-events hidden">
      <p>No events found on this page.</p>
      <button id="retry-no-events-btn" class="btn btn-secondary">Try Again</button>
    </div>

    <div id="events-container" class="events-container hidden">
      <div id="events-list"></div>
    </div>

    <div id="initial-state" class="initial-state">
      <div style="text-align: center; padding: 20px;">
        <button id="extract-btn" class="btn btn-primary">Extract Events from Page</button>
      </div>
    </div>
  </div>

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

```

### popup.css

```css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  width: 450px;
  min-height: 190px;
  max-height: 500px;
  overflow-y: auto;
  background: #f5f5f5;
}

.container {
  padding: 0;
  border-radius: 25px;
}

.top-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  background: #fff;
  border-bottom: 2px solid #e0e0e0;
  margin-bottom: 20px;
}

.top-left {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.top-bar h1 {
  font-size: 20px;
  color: #1a73e8;
  margin: 0;
}

.btn-small {
  font-size: 12px;
  padding: 8px 12px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.top-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}

.user-dropdown {
  position: relative;
}

.dropdown-menu {
  position: absolute;
  top: 100%;
  right: 0;
  background: white;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  min-width: 200px;
  z-index: 1000;
  margin-top: 4px;
}

.dropdown-menu.hidden {
  display: none;
}

.dropdown-item {
  padding: 0;
}

.dropdown-item:first-child {
  border-radius: 8px 8px 0 0;
}

.dropdown-item:last-child {
  border-radius: 0 0 8px 8px;
}

.user-email {
  padding: 12px 16px;
  font-size: 13px;
  color: #333;
  font-weight: 500;
  border-bottom: 1px solid #f0f0f0;
}

.dropdown-divider {
  height: 1px;
  background: #f0f0f0;
  margin: 0;
}

.dropdown-link {
  display: block;
  width: 100%;
  padding: 12px 16px;
  background: none;
  border: none;
  text-align: left;
  font-size: 13px;
  color: #1a73e8;
  cursor: pointer;
  transition: background-color 0.2s;
}

.dropdown-link:hover {
  background-color: #f8f9fa;
}

.btn-icon {
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 6px;
  background: #f1f3f4;
  color: #333;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  transition: all 0.2s;
}

.btn-icon:hover {
  background: #e0e0e0;
  transform: scale(1.05);
}

.btn-icon:active {
  transform: scale(0.95);
}

.container > *:not(.top-bar) {
  padding: 0 20px;
}

.hidden {
  display: none !important;
}

.loading {
  text-align: center;
  padding: 40px 20px;
}

.spinner {
  border: 3px solid #f3f3f3;
  border-top: 3px solid #3aced5;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: spin 1s linear infinite;
  margin: 0 auto 15px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.loading p {
  color: #666;
  font-size: 14px;
}

.error {
  background: #fee;
  border: 1px solid #fcc;
  border-radius: 8px;
  padding: 15px;
  margin-bottom: 15px;
}

.error-message {
  color: #c00;
  font-size: 14px;
  margin-bottom: 10px;
}

.no-events {
  text-align: center;
  padding: 30px 20px;
  background: #fff;
  border-radius: 8px;
  border: 2px dashed #ddd;
}

.no-events p {
  color: #666;
  margin-bottom: 15px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s;
  width: 100%;
}

.btn-primary {
  background: #3aced5;
  color: white;
}

.btn-primary:hover {
  background: #36bac1;
}

.btn-secondary {
  background: #f1f3f4;
  color: #333;
}

.btn-secondary:hover {
  background: #e0e0e0;
}

.btn-success {
  background: #3aced5;
  color: white;
}

.btn-success:hover {
  background: #36bac1;
}

.btn-success:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.initial-state {
  text-align: center;
  padding: 40px 0 60px 0;
}

.events-container {
  max-height: 450px;
  overflow-y: auto;
}

.event-card {
  background: white;
  border-radius: 8px;
  padding: 15px;
  margin-bottom: 15px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  transition: box-shadow 0.3s;
}

.event-card:hover {
  box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}

.event-card.expanded {
  border: 2px solid #b4fbff;
}

.event-header {
  margin-bottom: 10px;
}

.event-title {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

.event-preview {
  font-size: 13px;
  color: #666;
  margin-bottom: 10px;
}

.event-preview-item {
  margin-bottom: 5px;
  display: flex;
  align-items: center;
}

.event-preview-item::before {
  content: '•';
  margin-right: 8px;
  color: #1a73e8;
}

.event-details {
  display: none;
  margin-top: 15px;
  padding-top: 15px;
  border-top: 1px solid #e0e0e0;
}

.event-card.expanded .event-details {
  display: block;
}

.form-group {
  margin-bottom: 12px;
}

.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: #555;
  margin-bottom: 5px;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 13px;
  font-family: inherit;
}

.form-group textarea {
  resize: vertical;
  min-height: 60px;
}

.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-color: #1a73e8;
}

.datetime-group {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.event-actions {
  margin-top: 15px;
  display: flex;
  gap: 10px;
}

.event-actions .btn {
  flex: 1;
}

.success-message {
  background: #d4edda;
  border: 1px solid #c3e6cb;
  color: #155724;
  padding: 10px;
  border-radius: 4px;
  margin-top: 10px;
  font-size: 13px;
  text-align: center;
}

```

### popup.js

```javascript
const API_URL = 'https://planify-414894684325.us-east1.run.app:8080';

let extractedEvents = [];
let currentTab = null;
let currentUrl = null;

document.addEventListener('DOMContentLoaded', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  currentTab = tab;
  currentUrl = tab.url;

  // Check if user is authenticated
  await checkAuthentication();

  // Check if we have saved events for this URL
  await loadSavedEvents();

  // Add event listeners
  document.getElementById('extract-btn').addEventListener('click', extractEvents);
  document.getElementById('retry-btn').addEventListener('click', extractEvents);
  document.getElementById('retry-no-events-btn').addEventListener('click', extractEvents);
  
  // User dropdown functionality
  document.getElementById('user-btn').addEventListener('click', toggleUserDropdown);
      document.getElementById('switch-account-btn').addEventListener('click', async () => {
        // Clear all cached events when switching accounts
        await clearAllSavedEvents();
        // Open authentication page in popup
        const authWindow = window.open(
          `${API_URL}/auth/google`,
          'authWindow',
          'width=500,height=600,resizable=yes,scrollbars=yes,status=yes,toolbar=no,menubar=no,location=no'
        );
        
        // Check for authentication completion
        const checkAuth = setInterval(async () => {
          if (authWindow.closed) {
            clearInterval(checkAuth);
            // Wait a moment for server to process authentication
            setTimeout(async () => {
              await checkAuthentication();
              showInitialState();
            }, 1000);
          }
        }, 1000);
        
        // Close dropdown
        document.getElementById('user-dropdown-menu').classList.add('hidden');
      });
  
  document.getElementById('re-extract-btn').addEventListener('click', async () => {
    await clearSavedEvents();
    extractEvents();
  });
  
  // Close dropdown when clicking outside
  document.addEventListener('click', (event) => {
    const dropdown = document.getElementById('user-dropdown-menu');
    const userBtn = document.getElementById('user-btn');
    if (!userBtn.contains(event.target) && !dropdown.contains(event.target)) {
      dropdown.classList.add('hidden');
    }
  });
});

async function checkAuthentication() {
  try {
    const response = await fetch(`${API_URL}/auth/status`);
    const data = await response.json();
    
    if (!data.authenticated) {
      showAuthRequired();
    } else {
      // User is authenticated, try to get user's name
      await fetchUserInfo();
    }
  } catch (error) {
    console.error('Error checking authentication:', error);
    showAuthRequired();
  }
}

function toggleUserDropdown() {
  const dropdown = document.getElementById('user-dropdown-menu');
  dropdown.classList.toggle('hidden');
}

async function fetchUserInfo() {
  try {
    const response = await fetch(`${API_URL}/auth/user-info`);
    const data = await response.json();
    
    const userEmail = data.email || 'Not authenticated';
    document.getElementById('user-email').textContent = userEmail;
  } catch (error) {
    console.error('Error fetching user info:', error);
    document.getElementById('user-email').textContent = 'Not authenticated';
  }
}

async function loadSavedEvents() {
  try {
    const result = await chrome.storage.local.get([`events_${currentUrl}`]);
    const savedEvents = result[`events_${currentUrl}`];
    
    if (savedEvents && savedEvents.length > 0) {
      extractedEvents = savedEvents;
      
      // Check if user is still authenticated before showing events
      const isAuthenticated = await checkAuthenticationStatus();
      if (isAuthenticated) {
        displayEvents(savedEvents);
        addReExtractButton();
      } else {
        // Show events but with re-authentication option
        displayEventsWithAuthWarning(savedEvents);
      }
    }
  } catch (error) {
    console.error('Error loading saved events:', error);
  }
}

async function checkAuthenticationStatus() {
  try {
    const response = await fetch(`${API_URL}/auth/status`);
    const data = await response.json();
    return data.authenticated;
  } catch (error) {
    console.error('Error checking authentication:', error);
    return false;
  }
}

function displayEventsWithAuthWarning(events) {
  document.getElementById('loading').classList.add('hidden');
  document.getElementById('initial-state').classList.add('hidden');
  document.getElementById('error').classList.add('hidden');
  document.getElementById('no-events').classList.add('hidden');
  document.getElementById('events-container').classList.remove('hidden');

  const eventsList = document.getElementById('events-list');
  eventsList.innerHTML = '';

  // Add authentication warning at the top
  const authWarning = document.createElement('div');
  authWarning.className = 'auth-warning';
  authWarning.style.cssText = 'background: #fff3cd; color: #856404; padding: 12px; border-radius: 4px; margin-bottom: 15px; border: 1px solid #ffeaa7; text-align: center;';
  authWarning.innerHTML = `
    <div style="font-weight: bold; margin-bottom: 8px;">⚠️ Authentication Required</div>
    <button id="reconnect-btn" class="btn btn-primary" style="font-size: 12px; padding: 6px 12px; background-color: #3aced5;">🔗 Reconnect Google Calendar</button>
  `;
  
  eventsList.appendChild(authWarning);
  
  // Add reconnect button event listener
  document.getElementById('reconnect-btn').addEventListener('click', async () => {
    // Clear all cached events when switching accounts
    await clearAllSavedEvents();
    // Open authentication page in popup
    openAuthPopup();
  });

  events.forEach((event, index) => {
    const eventCard = createEventCard(event, index);
    eventsList.appendChild(eventCard);
    
    // Disable all action buttons
    const quickAddBtn = eventCard.querySelector('.header-actions .btn-su
[truncated — 26851 more characters]
```