# Project export: Safe and Slug

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 2024
- Tagline: Stay safe and snug when attending public events at UCSC or abroad.
- Devpost: https://devpost.com/software/safe-and-slug
- GitHub: https://github.com/steeevin88/SafeAndSlug
- Demo: https://safe-and-slug.vercel.app/
- Result: winner ([MLH - GoDaddy] Best Domain Name from GoDaddy Registry)
- Team: 4 GitHub contributor(s) — Mauricio Curiel (41 commits), Steven Le (27 commits), NotMyPersonalAccount (26 commits), mikieyx (17 commits)

## Devpost submission (written by the team)

### Inspiration

Whether it be music concerts, college parties, sporting activities, or other UCSC events, there's always a responsibility for organizers to have the resources to ensure attendees' safety. Our application aims to help distribute these resources as swiftly as possible, minimizing attendees' discomfort at these public events with discretion. Ultimately, we connect those who feel unsafe with organizers with the resources and capabilities to support them.

### What it does

Safe and Slug allows individuals to host and create events, or join already-created events through scanning an event-tied QR code. When users attend events and request assistance we forward their contact information to admins who can then assist as needed. Requiring authentication to join an event, admins are given greater flexibility with the information they have on their attendees. For example, health risks, allergy information, contact information, etc... are all provided, ensuring that in the case of an emergency, event admins can react accordingly. Users Register for an event View event details Instantaneously notify organizers of emergencies Share contact and medical information with event organizers Organizers Access to organizer dashboard with list of attendees, requests for support Create and share new events via event discovery page and QR code(s) Share real-time updates between multiple organizers Respond to emergency requests from event attendees

### How we built it

We deployed a NextJS application on Vercel, written in Typescript and styled using Tailwind and DaisyUI. For user login, we used Auth0. Additionally, to ensure users are accessing appropriate routes, we employed three techniques. First, we applied Auth0's middleware functions to protect all routes, ensuring all users accessing Safe and Slug will have logged in via Auth0. For event pages, we check user membership to an event role list as a means of displaying the correct page. Lastly, many routes require the user to have filled completed account registration. This includes their full name, contact information, and medical information. We created our own middleware-like wrapper to render the user registration form and soft redirect to the intended page after the user completes their account. Concerning storing user, event, and emergency data, various models were designed for a MongoDB Atlas database which we connected to and manipulated using both MongoDB Compass and the Prisma ORM. All database interactions are handled on the server side. The admin dashboard, however, requires real-time data to be retrieved and displayed as fast as possible. Many organizers will be accessing the dashboard at the same time. Using TCP and polling will put great stress on the server as we scale. We identified that WebSockets are the ideal solution, but Vercel doesn't necessarily support longstanding socket connections. As a result, we created an additional socket server built on Express hosted via Glitch. When admins connect to the admin dashboard, we establish a socket connection. On the other side, when users send an emergency notification, our application sends an HTTP request to the Express server, which then forwards emergency information to all socket connections for that specific event.

### Challenges we ran into

A majority of our team wasn't the most familiar with using NextJS's App Router, Typescript, and especially Auth0 authentication systems. Initially, setting up authentication across our application's routes with Auth0, defining correct schemas in Prisma, and understanding how to utilize Typescript proved difficult, but reading lots and lots of docs led us to an outcome that we're all extremely proud of. Another challenge we ran into was trying to provide real-time data to all of our users without overloading our server with excessive pulling. We identified sockets as our solution; however, since Vercel doesn't support long-term sockets (they expire), we had to create our own WebSocket server. This was entirely new to us and we had complications in figuring out how to host our server in the cloud with a static URL (to communicate with our production build).

### Accomplishments we're proud of

In past hackathons, our team members tended to struggle to meet deadlines alongside the difficulties of picking up new technologies and developing full-stack applications. This time around, we were able to develop and polish, to an extent, an MVP of "Safe and Slug" alongside a production build and a custom domain: safeandslug.us. We're proud to also have created our own socket server and middleware to overcome shortcomings in the tools we used. Ultimately, we're happy to have balanced our time successfully, planned, and divided work among team members in a manner that led to personal victories.

### What we learned

A big thing we learned is that user authentication is made a lot easier through services provided by companies like Auth0. In a previous hackathon, we'd spent a HUGE amount of time developing a login system; something that Auth0 allowed us to do in a fraction of the time. We also learned a great deal about type safety and the practical differences between client-side and server-side rendering; these were key in developing Safe and Slug. We also learned how sockets work and why they're used.

### What's next

Ideally, we'd like to expand our application to mobile development to make it easier for users to access the safety features of Safe and Slug. Additionally, we hope to expand how we retrieve events, expanding our reach outside of UCSC and Slug Hacks.

## README (from the GitHub repository)

Project created for CruzHacks 2024

View our [Devpost Submission](https://devpost.com/software/safe-and-slug)

![System Design](/systemDesign.png)

## Detected evidence (automated analysis)

Indexed codebase: 28 recognized source files, 41 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- MongoDB (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (35 of 35)

```
.eslintrc.json
.gitignore
next.config.mjs
package.json
postcss.config.js
prisma/schema.prisma
public/manifest.json
README.md
server/index.js
src/actions.ts
src/app/(loggedIn)/account/page.tsx
src/app/(loggedIn)/addEvent/page.tsx
src/app/(loggedIn)/events/[id]/adminEvent.tsx
src/app/(loggedIn)/events/[id]/page.tsx
src/app/(loggedIn)/events/[id]/userEvent.tsx
src/app/(loggedIn)/events/[id]/userList.tsx
src/app/(loggedIn)/events/page.tsx
src/app/(loggedIn)/layout.tsx
src/app/(loggedOut)/layout.tsx
src/app/(loggedOut)/page.tsx
src/app/api/auth/[auth0]/route.ts
src/app/globals.css
src/components/EventModal.tsx
src/components/Incident.tsx
src/components/MultiInput.tsx
src/components/Navbar.tsx
src/components/QRCode.tsx
src/components/UpdateAccountForm.tsx
src/components/UserEventManager.tsx
src/lib/prisma.ts
src/middleware.ts
src/util/enforce.tsx
src/util/getUserByEmail.ts
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @auth0/auth0-react@^2.2.4, @auth0/nextjs-auth0@^3.5.0, @ducanh2912/next-pwa@^10.2.2, @prisma/client@^5.8.1, @types/node@^20, @types/react@^18, @types/react-dom@^18, autoprefixer@^10.0.1, classnames@^2.5.1, cors@^2.8.5, daisyui@^4.6.0, dayjs@^1.11.10, eslint@^8, eslint-config-next@14.1.0, express@^4.18.2, next@14.1.0, postcss@^8, prisma@^5.8.1, react@^18, react-dom@^18, react-icons@^5.0.1, tailwindcss@^3.3.0, typescript@^5, webpack@^5.89.0, ws@^8.16.0, zod@^3.22.4

### Recent commits (newest first)

- fixed button resizing on mobile, readded metadata for (loggedIn)
- removed unintentional not-found page, fixed build issues
- Merge branch 'main' of github.com:steeevin88/CruzHacks
- remoes download button when user installs app and never renders the button when users can't download the app
- styling for landing page, close dropdown-nav on redirect
- removes added file
- Merge branch 'main' of github.com:steeevin88/CruzHacks
- fixes imports
- readded actions.ts, should fix prod
- undo breaking changes that disallow button from rendering
- make page rerender such that the button is visible
- conditionally display download only if applicable
- adds button to download app
- rewrites middleware to not block manifest and pwa related links
- moves all routes into group for logged in routes
- creates new home page with custom root layout
- moves actions out of app router
- refactors enforce to be used with home page as well
- moves events page
- renamed system design png

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

### package.json

```
{
  "name": "safeandslug",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "postinstall": "prisma generate"
  },
  "dependencies": {
    "@auth0/auth0-react": "^2.2.4",
    "@auth0/nextjs-auth0": "^3.5.0",
    "@ducanh2912/next-pwa": "^10.2.2",
    "@prisma/client": "^5.8.1",
    "classnames": "^2.5.1",
    "cors": "^2.8.5",
    "daisyui": "^4.6.0",
    "dayjs": "^1.11.10",
    "express": "^4.18.2",
    "next": "14.1.0",
    "react": "^18",
    "react-dom": "^18",
    "react-icons": "^5.0.1",
    "ws": "^8.16.0",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8",
    "eslint-config-next": "14.1.0",
    "postcss": "^8",
    "prisma": "^5.8.1",
    "tailwindcss": "^3.3.0",
    "typescript": "^5",
    "webpack": "^5.89.0"
  }
}

```

### server/index.js

```javascript
const http = require('http')
const express = require('express')
const WebSocket = require('ws')
const cors =require('cors')

const app = express();
app.use(cors())
app.use(express.json())

const server = http.createServer(app)
const wss = new WebSocket.Server({server});

app.post('/createEmergency', async function requestHandler(req, res){
    wss.clients.forEach(client => {
        if (client.eventId === req.body.event.id){
            client.send(JSON.stringify({
                type: "createEmergency",
                emergency: req.body.emergency,
                sound: req.body.sound
            }));
        }
    });
    res.status(200).json({});
})

app.post('/deleteEmergency', async function requestHandler(req, res){
    wss.clients.forEach(client => {
        if (client.eventId === req.body.event.id){
            client.send(JSON.stringify({
                type: "deleteEmergency",
                emergency: req.body.emergency
            }));
        }
    });
    res.status(200).json({});
})

app.post('/updateUsers', async function requestHandler(req, res){
    wss.clients.forEach(client => {
        if (client.eventId === req.body.event.id){
            client.send(JSON.stringify({
                type: "updateUsers",
                users: req.body.usersapp/(loggedIn)/
            }));
        }
    });
    res.status(200).json({});
})

wss.on('connection', function (w){
    console.log('New connection')

    w.onmessage = (e) => {
        w.eventId = e.data;
    }
})

server.listen(1337, function () {
    console.log('Server running')
})
```

### src/app/(loggedIn)/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "../globals.css";
import { UserProvider } from '@auth0/nextjs-auth0/client';
import Navbar from "@/components/Navbar";
import Enforce from "@/util/enforce";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Safe And Slug",
  description: "Attend UCSC events safe and snug!",
};

export default function RootLayout({
  children,

}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <UserProvider>
        <body className={`bg-white ${inter.className}`}>
          <Navbar loggedIn/>
          <Enforce>{children}</Enforce>
        </body>
      </UserProvider>
    </html>
  );
}

```

### src/app/(loggedOut)/page.tsx

```typescript
"use client"
import React, { useState } from 'react'

const Page = () => {
  type BeforeInstallPromptEvent = any;
  const [installPrompt, setInstallPrompt] = useState<null | BeforeInstallPromptEvent>(null)

  if (typeof window !== "undefined") {
    window.addEventListener("beforeinstallprompt", (event) => {
      event.preventDefault()
      setInstallPrompt(event)
    })
  }

  return (
    <div className='h-screen flex flex-col items-center justify-center gap-10'>
        <a href="/api/auth/login"><button className="btn btn-wide">Log In</button></a>
        {/* TODO: only render in non-standalone and if browser supports download */}
        {installPrompt && <button className="btn btn-wide" onClick={async () => {
          const userSelection = await installPrompt.prompt()
          console.log(userSelection)
          if (userSelection.outcome == "accepted") {setInstallPrompt(null)}
        }} >Download App</button>}
    </div>
  )
}

export default Page
```

### src/app/(loggedOut)/layout.tsx

```typescript
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import "../globals.css";
import { UserProvider } from '@auth0/nextjs-auth0/client';
import Navbar from "@/components/Navbar";
import Enforce from "@/util/enforce";

const inter = Inter({ subsets: ["latin"] });

const APP_NAME = "Safe And Slug";
const APP_DEFAULT_TITLE = "Safe And Slug";
const APP_TITLE_TEMPLATE = "%s - PWA App";
const APP_DESCRIPTION = "Attend UCSC events safe and sound!";

export const metadata: Metadata = {
  applicationName: APP_NAME,
  title: {
    default: APP_DEFAULT_TITLE,
    template: APP_TITLE_TEMPLATE,
  },
  description: APP_DESCRIPTION,
  manifest: "/manifest.json",
  appleWebApp: {
    capable: true,
    statusBarStyle: "default",
    title: APP_DEFAULT_TITLE,
    // startUpImage: [],
  },
  formatDetection: {
    telephone: false,
  },
  openGraph: {
    type: "website",
    siteName: APP_NAME,
    title: {
      default: APP_DEFAULT_TITLE,
      template: APP_TITLE_TEMPLATE,
    },
    description: APP_DESCRIPTION,
  },
  twitter: {
    card: "summary",
    title: {
      default: APP_DEFAULT_TITLE,
      template: APP_TITLE_TEMPLATE,
    },
    description: APP_DESCRIPTION,
  },
};

export const viewport: Viewport = {
  themeColor: "#FFFFFF",
};

export default function RootLayout({
  children,

}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <UserProvider>
        <body className={`bg-white ${inter.className}`}>
          <Navbar />
          <Enforce shouldRedirect>{children}</Enforce>
        </body>
      </UserProvider>
    </html>
  );
}

```

### src/app/(loggedIn)/account/page.tsx

```typescript
import React from 'react'
import UpdateAccountForm from "@/components/UpdateAccountForm";
import { getSession } from '@auth0/nextjs-auth0';
import { getUserByEmail } from '@/util/getUserByEmail';

export default async function UpdateAccount () {
  const email = (await getSession())!.user.email;
  const userInfo = await getUserByEmail(email);

  return <UpdateAccountForm initialValues={userInfo}/>
}

```

### src/app/(loggedIn)/addEvent/page.tsx

```typescript
import React from 'react';
import { useUser } from '@auth0/nextjs-auth0/client';
import prisma from '@/lib/prisma';
import MultiInput from '@/components/MultiInput';
import { getSession } from '@auth0/nextjs-auth0';
import { redirect } from 'next/navigation';
import Enforce from '@/util/enforce';


export default function AddEvent(){
    async function submitEvent(formData: FormData) {
        "use server"

        const session = await getSession();
        if (!session) {
            //TODO: Handle
            return;
        }


        await prisma.event.create({
            data: {
                title: formData.get("title") as string,
                description: formData.get("description") as string,
                location: formData.get("location") as string,
                admins: {
                    connect: [
                        {
                            email: session.user.email
                        },
                        ...((formData.get("admins") as string).split(",").filter(a => a).map(a => {
                            return {email: a};
                        }))
                    ]
                }
            }
        })
        redirect("/")
    }

    return(
        <div className='md:flex justify-center min-h-screen'>
            <div className='flex flex-col px-4 py-32 gap-8'>
                <h1 className='text-4xl font-bold text-gray-700'>Add Event</h1>
                <form className='flex flex-col gap-4 text-primary' action={submitEvent}>
                    <input className='input input-large' name="title" placeholder='Event Name' />
                    <input className='input input-large' name="description" placeholder='Description' />
                    <input className='input input-large' name="location" placeholder='Location' />
                    <MultiInput name='admins' type='email' placeholder='Admin (by email)'/>
                    <button className='btn' type="submit">
                        Add Event
                    </button>
                </form>
            </div>
        </div>
    )
};
```

### src/app/(loggedIn)/events/page.tsx

```typescript
import Link from "next/link";
import prisma from "../../../lib/prisma";
import { Event } from "@prisma/client";
import { getSession } from "@auth0/nextjs-auth0";
import { getUserByEmail } from "@/util/getUserByEmail";
import EventModal from "../../../components/EventModal";

async function getEvents() {
  const events = await prisma.event.findMany();
  return events;
}

type Props = {
  event: Event;
  joined: boolean;
  admin: boolean;
}

const EventComponent = ({ event, joined, admin}: Props) => {
  return(
      <div className="grid grid-cols-5 md:min-h-48 bg-primary p-4 pr-6 rounded-lg gap-2 text-primary-content justify-center items-center">
        <div className="flex flex-col col-span-3">
          <div className="text-h-full text-2xl truncate">
            {event.title}
          </div>
          <div className="h-[50%] md:h-auto truncate">
            {event.location}
          </div>
        </div>
        <a href={`/events/${event.id}`}>
          <button className="btn btn-secondary btn-sm md:btn-md col-span-2">
            {admin ? "Manage" : (joined ? "View" : "Join")} Event
          </button>
        </a>
        <EventModal event={event}/>
      </div>
  )
}

export default async function Home() {
  const email = (await getSession())!.user.email;
  const userInfo = (await getUserByEmail(email))!;
  const events = await getEvents();

  return (
    <main className="flex flex-col items-center min-h-[calc(100vh)] py-24">
      <h2 className="p-8 md:p-12 text-4xl text-center font-extrabold tracking-tight text-gray-700 lg:text-4xl">
        Slug it Out at Your Next Event...
      </h2>
      <div className="bg-gray-700 rounded-lg shadow-md
        w-[90%] md:w-[80%] grid lg:grid-cols-3 gap-2 overflow-y-auto  min-h-[calc(50vh)] p-4">
        {events.length > 0 ? (
          events.map((event) => (
            <EventComponent key={event.id} joined={event.userIds.includes(userInfo.id)} admin={event.adminIds.includes(userInfo.id)} event={event}/>
          ))
        ) : (
          <div className="flex flex-col col-span-3">
            <div className="text-3xl md:text-2xl text-center justify-center text-primary-content col-span-3 md:p-12">
              No events are currently available.
              <br/><br/>
              Click the button below to create a new event for others to join!
            </div>
            <div className="flex h-full justify-center items-center">
              <Link href="/addEvent" className="p-4">
                <button className="btn bg-secondary text-xl border-0">
                  Add Events
                </button>
              </Link>
            </div>
          </div>
        )}
      </div>
    </main>
  );
}

```

### src/app/api/auth/[auth0]/route.ts

```typescript
// from quickstart
// https://manage.auth0.com/dashboard/us/dev-ge7mucha16f8g3ib/applications/5pfiImbsAaWGT0eys1A10WVro68xJ2Bh/quickstart/nextjs
/*
This creates the following routes:

/api/auth/login: The route used to perform login with Auth0.
/api/auth/logout: The route used to log the user out.
/api/auth/callback: The route Auth0 will redirect the user to after a successful login.
/api/auth/me: The route to fetch the user profile from.

*/

// app/api/auth/[auth0]/route.js
import { handleAuth } from '@auth0/nextjs-auth0';

export const GET = handleAuth();
```

### src/app/(loggedIn)/events/[id]/page.tsx

```typescript
import prisma from "@/lib/prisma"
import AdminEvent from "./adminEvent"
import UserEvent from "./userEvent"
import { Session, getSession } from "@auth0/nextjs-auth0"
import { getUserByEmail } from "@/util/getUserByEmail"

export default async function Page({ params }: { params: { id: string} }) {
    const id = params.id
    const eventData = await prisma.event.findUnique({
        where: {
            id
        },
        include: {
            admins: true,
            users: true,
            emergencies: {
                orderBy: {
                    time: "desc"
                },
                include: {
                    user: true
                }
            }
        }
    })
    if (!eventData) {
        console.log("Cannot find event")
        return false    
    }

   const session : Session = (await getSession())!
   const user = (await getUserByEmail(session.user.email))!
   const admins = eventData.admins
   const users = eventData.users
   const isAdmin = admins.some(a => a.email == user.email)

   return (isAdmin ? <AdminEvent admins={admins} users={users} emergencies={eventData.emergencies} event={eventData}/> : <UserEvent event={eventData} user={user} />);
  }

```

[18 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]