├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── LICENSE.md
├── Makefile
├── README.md
├── docker-compose.dev.yaml
├── docker-compose.yaml
├── docs
├── example_api_response.json
├── spots.png
└── topdown.png
└── frontend
├── .env.template
├── .eslintrc.json
├── .gitignore
├── Dockerfile
├── README.md
├── app
├── api
│ └── open-spots
│ │ └── route.ts
├── favicon.ico
├── fonts
│ ├── GeistMonoVF.woff
│ └── GeistVF.woff
├── globals.css
├── layout.tsx
└── page.tsx
├── components.json
├── components
├── controls
│ ├── building-drawer
│ │ ├── building-drawer.module.css
│ │ ├── building-drawer.tsx
│ │ └── index.ts
│ ├── index.ts
│ └── map
│ │ ├── index.ts
│ │ └── map.tsx
└── ui
│ ├── accordion.tsx
│ ├── alert.tsx
│ ├── hover-card.tsx
│ ├── index.ts
│ ├── loading
│ ├── index.ts
│ └── loading.tsx
│ └── scroll-area.tsx
├── lib
├── helpers
│ ├── index.ts
│ └── map
│ │ ├── index.ts
│ │ └── map.helpers.ts
├── index.ts
├── services
│ ├── index.ts
│ └── map-data
│ │ ├── functions
│ │ ├── index.ts
│ │ └── map-data.ts
│ │ ├── index.ts
│ │ └── map-data.ts
├── types
│ ├── index.ts
│ └── map.types.ts
└── utils.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── images
│ └── github.png
└── logo.png
├── tailwind.config.ts
├── tsconfig.json
└── vercel.json
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Push Docker Images
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4
13 |
14 | - name: Login to Docker Hub
15 | uses: docker/login-action@v3
16 | with:
17 | username: ${{ secrets.DOCKERHUB_USERNAME }}
18 | password: ${{ secrets.DOCKERHUB_TOKEN }}
19 |
20 | - name: Set up Docker Buildx
21 | uses: docker/setup-buildx-action@v3
22 |
23 | - name: Build and push frontend image
24 | uses: docker/build-push-action@v5
25 | with:
26 | context: ./frontend
27 | file: frontend/Dockerfile
28 | push: true
29 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{ github.sha }}
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | docker-compose.dev.test.yaml
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Akshar Barot
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | docker-compose build
3 |
4 | build-force:
5 | docker-compose build --no-cache
6 |
7 | up:
8 | docker-compose -f docker-compose.yaml up -d
9 |
10 | up-dev:
11 | docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml up -d
12 |
13 | down:
14 | docker-compose down
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | # Open Spots
7 |
8 | **Open Spots** is a fork of [Spots](https://github.com/notAkki/spots) that is designed to help organizations deliver real-time building availability data to staff, employees, customers, or students. Developers can create an API to deliver their own data to the Open Spots platform. This data can be visualized on an interactive map, and display available spots (rooms with time slots).
9 |
10 | 
11 | 
12 |
13 | ## Features
14 |
15 | - Displays open spots across an organization's building or campus.
16 | - Sorts spots based on proximity to the user’s current location.
17 | - Interactive map to visualize building locations.
18 | - List view of spots with real-time status updates (based on the organization's data).
19 |
20 | ## Tech Stack
21 |
22 | ### Frontend
23 |
24 | - **Next.js**: Handles server-side rendering and provides a robust React-based framework for building the frontend UI.
25 | - **Mapbox GL**: Provides the interactive map to display building locations.
26 | - **Tailwind CSS**: Used for styling the UI components with utility-first CSS for responsive and consistent design.
27 | - **Geolocation API**: Retrieves the user’s current location to sort spots by proximity.
28 |
29 | ### Backend
30 |
31 | - **Flask**: A lightweight Python web framework to handle API requests and logic for retrieving and processing spot data.
32 | - **Requests**: A Python library used in Flask to fetch spot data from external APIs.
33 | - **Haversine Formula**: Implemented in the backend to calculate the distance between the user and spot locations based on coordinates.
34 |
35 |
36 | ## API
37 |
38 | There are a few different variables that can be set in the `/frontend/.env` file to customize Open Spots.
39 |
40 | - `API_URL`: The URL of the API (with the endpoint) that will be used to fetch spot data. **Required**
41 | - `NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN`: The Mapbox access token to be used for the map. **Required**
42 | - `NEXT_PUBLIC_MAPBOX_STYLE_URL`: The Mapbox style URL to be used for the map. **Required**
43 | - `NEXT_PUBLIC_STARTING_CENTER_COORDS`: The starting center coordinates to be used for the map.
44 | - `NEXT_PUBLIC_STARTING_ZOOM`: The starting zoom level to be used for the map.
45 | - `NEXT_PUBLIC_STARTING_PITCH`: The starting pitch level to be used for the map.
46 | - `NEXT_PUBLIC_SITE_TITLE`: The title of the site to be used in the header.
47 | - `NEXT_PUBLIC_SITE_DESCRIPTION`: The description of the site to be used in the header.
48 |
49 | Your API should return a JSON object with the following fields:
50 |
51 | - `logo`: The URL of the logo to be used in the header. **Optional**
52 | - `data`: An array of building data.
53 |
54 | The building data has the following fields:
55 |
56 | - `building`: The name of the building.
57 | - `building_code`: The code of the building.
58 | - `building_status`: The status of the building.
59 | - `rooms`: A dictionary of room numbers to room data.
60 | - `coords`: The coordinates of the building.
61 | - `distance`: The distance to the building from the user's current location.
62 | - `distance_unit`: The unit of distance ("mi" or "km")
63 |
64 | The room data has the following fields:
65 |
66 | - `roomNumber`: The number of the room.
67 | - `slots`: An array of time slots for the room.
68 |
69 | The slots data has the following fields:
70 |
71 | - `StartTime`: The start time of the time slot.
72 | - `EndTime`: The end time of the time slot.
73 | - `Status`: The status of the time slot.
74 |
75 | An example API response can be found in `docs/example_api_response.json`.
76 |
77 | The Open Spots API sends a POST (if the user has geolocation enabled) or GET request to the API URL set in the `/frontend/.env` file.
78 |
79 | The post request body is a JSON object with the following fields:
80 |
81 | - `lat`: The latitude of the user's current location.
82 | - `lng`: The longitude of the user's current location.
83 |
84 | ## Local Setup
85 |
86 | 1. Install Docker and Docker Compose.
87 | 2. Run `make up` to start Open Spots.
88 |
--------------------------------------------------------------------------------
/docker-compose.dev.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | frontend:
3 | command: npm run dev
4 | volumes:
5 | - ./frontend/app:/app/app
6 | - ./frontend/components:/app/components
7 | - ./frontend/lib:/app/lib
8 | - ./frontend/public:/app/public
9 | - ./frontend/package.json:/app/package.json
10 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | frontend:
3 | container_name: open-spots
4 | restart: unless-stopped
5 | env_file: ./frontend/.env
6 | image: jpyles0524/open-spots
7 | build:
8 | context: ./frontend
9 | dockerfile: Dockerfile
10 | command: npm run start
11 | ports:
12 | - 3000:3000
--------------------------------------------------------------------------------
/docs/example_api_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "logo": "**Optional** Your Logo URL",
3 | "data": [
4 | {
5 | "building": "Your Building Name",
6 | "building_code": "Your Building Code",
7 | "building_status": "available",
8 | "coords": [0, 0],
9 | "distance": 0,
10 | "distance_unit": "mi",
11 | "labels": [
12 | {
13 | "label": "Your Label",
14 | "color": "**Optional** Your Color (HEX VALUE)"
15 | }
16 | ],
17 | "rooms": [
18 | {
19 | "roomNumber": "Your Room Number",
20 | "slots": [
21 | {
22 | "EndTime": "Your End Time",
23 | "StartTime": "Your Start Time",
24 | "Status": "Your Status"
25 | }
26 | ]
27 | }
28 | ]
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/docs/spots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaypyles/open-spots/8026c9e00b0cc404da8e4f02cca8b64c340038c7/docs/spots.png
--------------------------------------------------------------------------------
/docs/topdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaypyles/open-spots/8026c9e00b0cc404da8e4f02cca8b64c340038c7/docs/topdown.png
--------------------------------------------------------------------------------
/frontend/.env.template:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=
2 | NEXT_PUBLIC_MAPBOX_STYLE_URL=mapbox://styles/jpyles0524/cm2qof1hq00cp01qi2cd69r16
3 | NEXT_PUBLIC_STARTING_CENTER_COORDS=,
4 | NEXT_PUBLIC_STARTING_ZOOM=
5 | NEXT_PUBLIC_STARTING_PITCH=
6 | API_URL=/ # this will be the endpoint to receive data from your backend
7 | NEXT_PUBLIC_SITE_TITLE=
8 | NEXT_PUBLIC_SITE_DESCRIPTION=
9 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | .env
39 |
40 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1: Dependencies
2 | FROM node:21 AS depsbuilder
3 |
4 | # Set the working directory in the container
5 | WORKDIR /app
6 |
7 | # Copy only the package files to install dependencies
8 | COPY package.json package-lock.json /app/
9 |
10 | # Install the dependencies
11 | RUN npm install
12 |
13 | # Stage 2: Build
14 | FROM node:21
15 |
16 | # Set the working directory in the container
17 | WORKDIR /app
18 |
19 | # Copy application files and configuration
20 | COPY app /app/app
21 | COPY components /app/components
22 | COPY lib /app/lib
23 | COPY public /app/public
24 | COPY tsconfig.json /app
25 | COPY components.json /app
26 | COPY next.config.mjs /app
27 | COPY tailwind.config.ts /app
28 | COPY postcss.config.mjs /app
29 | COPY package.json package-lock.json /app/
30 |
31 | # Copy installed node_modules from the depsbuilder stage
32 | COPY --from=depsbuilder /app/node_modules /app/node_modules
33 |
34 | # Build the application
35 | RUN npm run build
36 |
37 | # Expose the port that your app runs on
38 | EXPOSE 3000
39 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Spots Frontend
2 |
3 | ## Local Development
4 |
5 | 1. Install Docker and Docker Compose.
6 | 2. Run `make build up-dev` to start the frontend for local development.
7 |
8 | ### Environment Variables
9 |
10 | Copy these into a `.env` file in the frontend directory using `.env.example` as a template.
11 |
12 | ```bash
13 | cp .env.example .env
14 | ```
--------------------------------------------------------------------------------
/frontend/app/api/open-spots/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { type MapData } from "@/lib";
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | const { lat, lng } = await req.json();
7 |
8 | const response = await fetch(process.env.API_URL as string, {
9 | method: "POST",
10 | headers: {
11 | "Content-Type": "application/json",
12 | },
13 | body: JSON.stringify({ lat, lng }),
14 | });
15 |
16 | if (!response.ok) {
17 | return NextResponse.json(
18 | { error: "Failed to fetch data" },
19 | { status: 500 }
20 | );
21 | }
22 |
23 | const data: MapData[] = await response.json();
24 | return NextResponse.json(data);
25 | } catch (error) {
26 | console.error("Error in route:", error);
27 | return NextResponse.json(
28 | { error: "Failed to process request" },
29 | { status: 500 }
30 | );
31 | }
32 | }
33 |
34 | export async function GET() {
35 | try {
36 | const response = await fetch(process.env.API_URL as string, {
37 | method: "GET",
38 | cache: "no-cache",
39 | });
40 |
41 | if (!response.ok) {
42 | return NextResponse.json(
43 | { error: "Failed to fetch data" },
44 | { status: 500 }
45 | );
46 | }
47 |
48 | const data: MapData[] = await response.json();
49 | return NextResponse.json(data);
50 | } catch (error) {
51 | console.error("Error in GET route:", error);
52 | return NextResponse.json(
53 | { error: "Failed to process request" },
54 | { status: 500 }
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaypyles/open-spots/8026c9e00b0cc404da8e4f02cca8b64c340038c7/frontend/app/favicon.ico
--------------------------------------------------------------------------------
/frontend/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaypyles/open-spots/8026c9e00b0cc404da8e4f02cca8b64c340038c7/frontend/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/frontend/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaypyles/open-spots/8026c9e00b0cc404da8e4f02cca8b64c340038c7/frontend/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/frontend/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #0a0a0f;
7 | --foreground: #f4f4f5;
8 | --popup-background: #18181bbe;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --background: #18181b;
14 | --foreground: #ededed;
15 | }
16 | }
17 |
18 | body {
19 | color: var(--foreground);
20 | background: var(--background);
21 | font-family: Arial, Helvetica, sans-serif;
22 | }
23 |
24 | @layer utilities {
25 | .text-balance {
26 | text-wrap: balance;
27 | }
28 | }
29 |
30 | @layer base {
31 | :root {
32 | --radius: 0.5rem;
33 | }
34 | }
35 |
36 | #map-container {
37 | height: 100%;
38 | width: 100%;
39 | background-color: lightgrey;
40 | border-radius: 20px;
41 | }
42 |
43 | .marker-popup {
44 | background-color: var(--popup-background);
45 | border-radius: 10px;
46 | padding: 0.25rem;
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 |
5 | const geistSans = localFont({
6 | src: "./fonts/GeistVF.woff",
7 | variable: "--font-geist-sans",
8 | weight: "100 900",
9 | });
10 | const geistMono = localFont({
11 | src: "./fonts/GeistMonoVF.woff",
12 | variable: "--font-geist-mono",
13 | weight: "100 900",
14 | });
15 |
16 | export const metadata: Metadata = {
17 | title: process.env.NEXT_PUBLIC_SITE_TITLE || "Open Spots",
18 | description:
19 | process.env.NEXT_PUBLIC_SITE_DESCRIPTION ||
20 | "Track open rooms between buildings.",
21 | };
22 |
23 | export default function RootLayout({
24 | children,
25 | }: Readonly<{
26 | children: React.ReactNode;
27 | }>) {
28 | return (
29 |
30 |
33 | {children}
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect, useState } from "react";
3 | import { ScrollArea } from "@/components/ui/scroll-area";
4 | import Image from "next/image";
5 | import { Map as MapComponent } from "@/components/controls/map/map";
6 | import { Loading } from "@/components/ui";
7 | import { BuildingDrawer } from "@/components/controls";
8 | import { APIResponse, type MapData } from "@/lib";
9 | import { mapDataService } from "@/lib/services";
10 |
11 | export default function OpenSpots() {
12 | const [logo, setLogo] = useState(null);
13 | const [data, setData] = useState([]);
14 | const [activeBuilding, setActiveBuilding] = useState(null);
15 | const [userPos, setUserPos] = useState<[number, number] | null>(null);
16 | const [loading, setLoading] = useState(true);
17 | const startingCenterCoords = process.env.NEXT_PUBLIC_STARTING_CENTER_COORDS
18 | ? (process.env.NEXT_PUBLIC_STARTING_CENTER_COORDS.split(",").map(
19 | Number
20 | ) as [number, number])
21 | : undefined;
22 | const startingZoom = process.env.NEXT_PUBLIC_STARTING_ZOOM
23 | ? Number(process.env.NEXT_PUBLIC_STARTING_ZOOM)
24 | : undefined;
25 | const startingPitch = process.env.NEXT_PUBLIC_STARTING_PITCH
26 | ? Number(process.env.NEXT_PUBLIC_STARTING_PITCH)
27 | : undefined;
28 |
29 | const handleMarkerClick = (building: string) => {
30 | setActiveBuilding(building);
31 | };
32 |
33 | useEffect(() => {
34 | const fetchLocationAndData = async () => {
35 | setLoading(true);
36 |
37 | if (navigator.geolocation) {
38 | navigator.geolocation.getCurrentPosition(
39 | async (position) => {
40 | const { latitude, longitude } = position.coords;
41 | setUserPos([latitude, longitude]);
42 |
43 | const data: APIResponse = await mapDataService.sendUserLocation(
44 | latitude,
45 | longitude
46 | );
47 | setData(data.data);
48 | setLogo(data.logo || null);
49 | setLoading(false);
50 | },
51 | async (error) => {
52 | console.error("Error fetching location here:", error);
53 | const defaultData: APIResponse =
54 | await mapDataService.sendDefaultLocationData();
55 | setData(defaultData.data);
56 | setLogo(defaultData.logo || null);
57 | setLoading(false);
58 | }
59 | );
60 | } else {
61 | console.error("Geolocation is not supported by this browser.");
62 | const defaultData: APIResponse =
63 | await mapDataService.sendDefaultLocationData();
64 | setData(defaultData.data);
65 | setLogo(defaultData.logo || null);
66 | setLoading(false);
67 | }
68 | };
69 |
70 | fetchLocationAndData();
71 | }, []);
72 |
73 | if (loading) {
74 | return ;
75 | }
76 |
77 | return (
78 |
79 |
80 |
81 |
82 | {logo &&

}
83 |
84 |
85 |
97 |
98 |
107 |
108 |
109 |
117 |
118 |
119 | );
120 | }
121 |
--------------------------------------------------------------------------------
/frontend/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": false,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/frontend/components/controls/building-drawer/building-drawer.module.css:
--------------------------------------------------------------------------------
1 | .available {
2 | background-color: #34d399;
3 | }
4 |
5 | .opening-soon {
6 | background-color: #34d39991;
7 | }
8 |
9 | .unavailable {
10 | background-color: #f87171;
11 | }
12 |
13 | .closing-soon {
14 | background-color: #f8717191;
15 | }
16 |
17 | .upcoming {
18 | background-color: #fbbd23;
19 | }
20 |
21 | .available-label {
22 | background-color: rgb(22 101 52 / 0.3);
23 | color: rgb(134 239 172 / 0.9);
24 | }
25 |
26 | .unavailable-label {
27 | background-color: rgb(185 28 28 / 0.3);
28 | color: rgb(252 165 165 / 0.9);
29 | }
30 |
31 | .upcoming-label {
32 | background-color: rgb(146 64 14 / 0.3);
33 | color: rgb(252 211 77 / 0.9);
34 | }
35 |
36 | .distance {
37 | font-size: 0.825rem;
38 | color: lightgray;
39 | }
40 |
41 | .tags {
42 | display: flex;
43 | gap: 0.5rem;
44 | }
45 |
46 | .tag {
47 | padding: 0.25rem;
48 | line-height: 1;
49 | border-radius: 0.25rem;
50 | font-size: 0.75rem;
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/components/controls/building-drawer/building-drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import {
3 | Accordion,
4 | AccordionContent,
5 | AccordionItem,
6 | AccordionTrigger,
7 | } from "@/components/ui/accordion";
8 | import {
9 | formatTime,
10 | roundDistanceToHundreds,
11 | sliceString,
12 | type MapData,
13 | } from "@/lib";
14 | import { clsx } from "clsx";
15 | import classes from "./building-drawer.module.css";
16 |
17 | export const statusLabel = (status: string) => {
18 | return (
19 |
26 | {status}
27 |
28 | );
29 | };
30 |
31 | function statusIndicator(status: string) {
32 | return (
33 |
40 | );
41 | }
42 |
43 | export type BuildingDrawerProps = {
44 | data: MapData[];
45 | activeBuilding: string | null;
46 | setActiveBuilding: (building: string) => void;
47 | };
48 |
49 | export const BuildingDrawer = ({
50 | data,
51 | activeBuilding,
52 | setActiveBuilding,
53 | }: BuildingDrawerProps) => {
54 | const getBuildingType = (building: MapData) => {
55 | if (!building.labels) {
56 | return null;
57 | }
58 |
59 | return building.labels.map((label) => (
60 |
65 | {label.label}
66 |
67 | ));
68 | };
69 |
70 | return (
71 |
72 |
setActiveBuilding(val)}
78 | >
79 | {data.map((building) => (
80 |
86 |
87 |
88 |
89 |
90 |
91 | {building.building}
92 |
93 |
94 | {getBuildingType(building)}
95 |
96 |
97 |
98 | {roundDistanceToHundreds(building.distance)}{" "}
99 | {building.distance_unit}
100 |
101 |
102 |
{statusLabel(building.building_status)}
103 |
104 |
105 |
106 | {building.rooms &&
107 | Object.entries(building.rooms).map(([roomNumber, room]) => {
108 | return (
109 |
113 |
114 |
115 |
116 | {sliceString(
117 | `${building.building_code} ${room.roomNumber}`,
118 | 60
119 | )}
120 |
121 |
122 |
123 | {room.slots && room.slots.length > 0 ? (
124 | statusIndicator(room.slots[0].Status)
125 | ) : (
126 |
127 | No slots available
128 |
129 | )}
130 |
131 |
132 |
133 | {room.slots && room.slots.length > 0 ? (
134 | room.slots.map((slot, index) => (
135 | -
136 | {formatTime(slot.StartTime)} -{" "}
137 | {formatTime(slot.EndTime)}
138 |
139 | ))
140 | ) : (
141 | - No slots available
142 | )}
143 |
144 |
145 | );
146 | })}
147 |
148 |
149 | ))}
150 |
151 |
152 | );
153 | };
154 |
--------------------------------------------------------------------------------
/frontend/components/controls/building-drawer/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./building-drawer";
2 |
--------------------------------------------------------------------------------
/frontend/components/controls/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map";
2 | export * from "./building-drawer";
3 |
--------------------------------------------------------------------------------
/frontend/components/controls/map/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map";
2 |
--------------------------------------------------------------------------------
/frontend/components/controls/map/map.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { useRef, useEffect, useState } from "react";
4 | import mapboxgl from "mapbox-gl";
5 | import "mapbox-gl/dist/mapbox-gl.css";
6 | import { MapData } from "@/lib/types";
7 | import { createMarkerFromMapData } from "@/lib/helpers";
8 |
9 | export type MapProps = {
10 | data: MapData[];
11 | handleMarkerClick: (building: string) => void;
12 | userPos: [number, number] | null;
13 | startingCenterCoords?: [number, number];
14 | startingZoom?: number;
15 | startingPitch?: number;
16 | };
17 |
18 | export const Map = ({
19 | data,
20 | handleMarkerClick,
21 | userPos,
22 | startingCenterCoords = [0, 0],
23 | startingZoom = 100,
24 | startingPitch = 100,
25 | }: MapProps) => {
26 | const mapRef = useRef(null);
27 | const mapContainerRef = useRef(null);
28 | const [center, setCenter] = useState<[number, number]>(startingCenterCoords);
29 | const [zoom, setZoom] = useState(startingZoom);
30 | const [pitch, setPitch] = useState(startingPitch);
31 | const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN;
32 | const MAPBOX_STYLE_URL = process.env.NEXT_PUBLIC_MAPBOX_STYLE_URL;
33 | // const MAPBOX_STYLE_URL = "mapbox://styles/michaelgathara/cm2r2794200gj01phd50hflad";
34 |
35 | useEffect(() => {
36 | if (!mapboxToken) {
37 | throw new Error("Mapbox token is not defined");
38 | }
39 |
40 | mapboxgl.accessToken = mapboxToken;
41 | mapRef.current = new mapboxgl.Map({
42 | style: MAPBOX_STYLE_URL,
43 | container: mapContainerRef.current as HTMLElement,
44 | center: center,
45 | zoom: zoom,
46 | pitch: pitch,
47 | });
48 |
49 | mapRef.current.on("move", () => {
50 | if (mapRef.current) {
51 | const mapCenter = mapRef.current.getCenter();
52 | const mapZoom = mapRef.current.getZoom();
53 | const mapPitch = mapRef.current.getPitch();
54 |
55 | setCenter([mapCenter.lng, mapCenter.lat]);
56 | setZoom(mapZoom);
57 | setPitch(mapPitch);
58 | }
59 | });
60 |
61 | data.map((data) => {
62 | const buildingMarker = createMarkerFromMapData(data, handleMarkerClick);
63 |
64 | if (mapRef.current && data.coords) {
65 | const marker = new mapboxgl.Marker(buildingMarker)
66 | .setLngLat([data.coords[0], data.coords[1]])
67 | .addTo(mapRef.current);
68 |
69 | // Create a popup div for the building name
70 | const popup = document.createElement("div");
71 | popup.className = "marker-popup";
72 | popup.innerText = data.building;
73 |
74 | // Add hover effect for building markers
75 | marker.getElement().addEventListener("mouseenter", () => {
76 | marker.getElement().style.cursor = "pointer";
77 | const markerElement = marker.getElement();
78 | const markerRect = markerElement.getBoundingClientRect();
79 | const mapContainerRect =
80 | mapContainerRef.current?.getBoundingClientRect();
81 |
82 | markerElement.style.cursor = "pointer";
83 |
84 | if (mapContainerRect) {
85 | popup.style.zIndex = "1000";
86 | popup.style.left = `${
87 | markerRect.left - mapContainerRect.left - 20
88 | }px`;
89 | popup.style.top = `${
90 | markerRect.top - mapContainerRect.top - popup.offsetHeight - 30
91 | }px`;
92 | }
93 | popup.style.display = "block";
94 | popup.style.position = "absolute";
95 | popup.style.zIndex = "1000";
96 | mapContainerRef.current?.appendChild(popup);
97 | });
98 |
99 | marker.getElement().addEventListener("mouseleave", () => {
100 | marker.getElement().style.cursor = "";
101 | popup.style.display = "none";
102 | if (popup.parentNode) {
103 | popup.parentNode.removeChild(popup);
104 | }
105 | });
106 | }
107 | });
108 |
109 | if (userPos) {
110 | const userMarker = document.createElement("div");
111 | userMarker.className =
112 | "h-3 w-3 border-[1.5px] border-zinc-50 rounded-full bg-blue-400 shadow-[0px_0px_4px_2px_rgba(14,165,233,1)]";
113 | const userMarkerInstance = new mapboxgl.Marker(userMarker)
114 | .setLngLat([userPos[1], userPos[0]])
115 | .addTo(mapRef.current);
116 |
117 | userMarkerInstance.getElement().addEventListener("mouseenter", () => {
118 | userMarkerInstance.getElement().style.cursor = "pointer";
119 | });
120 | userMarkerInstance.getElement().addEventListener("mouseleave", () => {
121 | userMarkerInstance.getElement().style.cursor = "";
122 | });
123 | }
124 |
125 | return () => {
126 | if (mapRef.current) {
127 | mapRef.current.remove();
128 | }
129 | };
130 | }, []);
131 |
132 | return (
133 |
134 |
135 |
136 |
137 |
138 |
139 | unavailable
140 |
141 |
142 |
143 |
144 |
145 | closing soon
146 |
147 |
148 |
149 |
150 |
151 | opening soon
152 |
153 |
154 |
155 |
156 |
157 | open now
158 |
159 |
160 |
161 |
162 | );
163 | };
164 |
--------------------------------------------------------------------------------
/frontend/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { ChevronDownIcon } from "@radix-ui/react-icons";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
56 |
57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
58 |
--------------------------------------------------------------------------------
/frontend/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border border-zinc-700 px-3 py-2 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-zinc-950 [&>svg~*]:pl-7 dark:border-zinc-800 dark:[&>svg]:text-zinc-50",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-zinc-950/0 text-zinc-50 dark:bg-zinc-950 dark:text-zinc-50",
13 | destructive:
14 | "border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900",
15 | },
16 | },
17 | defaultVariants: {
18 | variant: "default",
19 | },
20 | }
21 | );
22 |
23 | const Alert = React.forwardRef<
24 | HTMLDivElement,
25 | React.HTMLAttributes & VariantProps
26 | >(({ className, variant, ...props }, ref) => (
27 |
33 | ));
34 | Alert.displayName = "Alert";
35 |
36 | const AlertTitle = React.forwardRef<
37 | HTMLParagraphElement,
38 | React.HTMLAttributes
39 | >(({ className, ...props }, ref) => (
40 |
45 | ));
46 | AlertTitle.displayName = "AlertTitle";
47 |
48 | const AlertDescription = React.forwardRef<
49 | HTMLParagraphElement,
50 | React.HTMLAttributes
51 | >(({ className, ...props }, ref) => (
52 |
57 | ));
58 | AlertDescription.displayName = "AlertDescription";
59 |
60 | export { Alert, AlertTitle, AlertDescription };
61 |
--------------------------------------------------------------------------------
/frontend/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ));
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent };
30 |
--------------------------------------------------------------------------------
/frontend/components/ui/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./loading";
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/loading/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./loading";
2 |
--------------------------------------------------------------------------------
/frontend/components/ui/loading/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const Loading = () => {
4 | return (
5 |
6 |
22 |
Loading...
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/frontend/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef<
29 | typeof ScrollAreaPrimitive.ScrollAreaScrollbar
30 | >
31 | >(({ className, orientation = "vertical", ...props }, ref) => (
32 |
45 |
46 |
47 | ));
48 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
49 |
50 | export { ScrollArea, ScrollBar };
51 |
--------------------------------------------------------------------------------
/frontend/lib/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map";
2 |
--------------------------------------------------------------------------------
/frontend/lib/helpers/map/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map.helpers";
2 |
--------------------------------------------------------------------------------
/frontend/lib/helpers/map/map.helpers.ts:
--------------------------------------------------------------------------------
1 | import { MapData } from "@/lib/types";
2 |
3 | export const STATUS_TO_COLOR = {
4 | available:
5 | "h-2 w-2 rounded-full bg-green-400 shadow-[0px_0px_4px_2px_rgba(34,197,94,0.7)]",
6 | unavailable:
7 | "h-2 w-2 rounded-full bg-red-400 shadow-[0px_0px_4px_2px_rgba(239,68,68,0.9)]",
8 | upcoming:
9 | "h-2 w-2 rounded-full bg-amber-400 shadow-[0px_0px_4px_2px_rgba(245,158,11,0.9)]",
10 | };
11 |
12 | export const GLOW_EFFECT_FROM_STATUS = {
13 | available: "0 0 10px rgba(34,197,94,0.7)",
14 | unavailable: "0 0 10px rgba(239,68,68,0.9)",
15 | upcoming: "0 0 10px rgba(245,158,11,0.9)",
16 | };
17 |
18 | export const DRAWER_STATUS_TO_COLOR = {
19 | available: "bg-green-800/20 text-green-300/90",
20 | unavailable: "bg-red-700/20 text-red-300/80",
21 | upcoming: "bg-amber-800/20 text-amber-300/90",
22 | };
23 |
24 | export const createMarkerFromMapData = (
25 | data: MapData,
26 | handleMarkerClick: (building: string) => void
27 | ) => {
28 | const el = document.createElement("div");
29 | el.className =
30 | STATUS_TO_COLOR[data.building_status as keyof typeof STATUS_TO_COLOR];
31 |
32 | el.style.width = "17.5px";
33 | el.style.height = "17.5px";
34 | el.style.boxShadow =
35 | GLOW_EFFECT_FROM_STATUS[
36 | data.building_status as keyof typeof GLOW_EFFECT_FROM_STATUS
37 | ];
38 |
39 | el.addEventListener("click", () => {
40 | const accordionItem = document.getElementById(data.building_code);
41 |
42 | setTimeout(() => {
43 | if (accordionItem) {
44 | accordionItem.scrollIntoView({
45 | behavior: "smooth",
46 | block: "start",
47 | });
48 | }
49 | }, 300);
50 |
51 | handleMarkerClick(data.building_code);
52 | });
53 |
54 | return el;
55 | };
56 |
--------------------------------------------------------------------------------
/frontend/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./helpers";
2 | export * from "./types";
3 | export * from "./utils";
4 |
--------------------------------------------------------------------------------
/frontend/lib/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map-data";
2 |
--------------------------------------------------------------------------------
/frontend/lib/services/map-data/functions/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map-data";
2 |
--------------------------------------------------------------------------------
/frontend/lib/services/map-data/functions/map-data.ts:
--------------------------------------------------------------------------------
1 | export const sendUserLocation = async (latitude: number, longitude: number) => {
2 | try {
3 | const res = await fetch(`/api/open-spots`, {
4 | method: "POST",
5 | headers: {
6 | "Content-Type": "application/json",
7 | },
8 | body: JSON.stringify({
9 | lat: latitude,
10 | lng: longitude,
11 | }),
12 | });
13 |
14 | const data = await res.json();
15 | return data;
16 | } catch (error) {
17 | console.error("Failed to fetch data from backend:", error);
18 | }
19 | };
20 |
21 | export const sendDefaultLocationData = async () => {
22 | const response = await fetch("/api/open-spots");
23 | const data = await response.json();
24 | return data;
25 | };
26 |
--------------------------------------------------------------------------------
/frontend/lib/services/map-data/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map-data";
2 |
--------------------------------------------------------------------------------
/frontend/lib/services/map-data/map-data.ts:
--------------------------------------------------------------------------------
1 | import { sendUserLocation, sendDefaultLocationData } from "./functions";
2 |
3 | export const mapDataService = {
4 | sendUserLocation,
5 | sendDefaultLocationData,
6 | };
7 |
--------------------------------------------------------------------------------
/frontend/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./map.types";
--------------------------------------------------------------------------------
/frontend/lib/types/map.types.ts:
--------------------------------------------------------------------------------
1 | export type APIResponse = {
2 | data: MapData[];
3 | logo?: string;
4 | };
5 |
6 | export type MapData = {
7 | building: string;
8 | building_code: string;
9 | building_status: string;
10 | rooms: Record;
11 | coords: [number, number];
12 | distance: number;
13 | distance_unit: string;
14 | labels?: Label[];
15 | logo?: string;
16 | };
17 |
18 | export type Label = {
19 | label: string;
20 | color?: string;
21 | };
22 |
23 | export type Slot = {
24 | StartTime: string;
25 | EndTime: string;
26 | Status: string;
27 | };
28 |
29 | export type Room = {
30 | roomNumber: string;
31 | slots: Slot[];
32 | };
33 |
--------------------------------------------------------------------------------
/frontend/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function formatTime(timeString: string) {
9 | const options = {
10 | hour: "numeric" as "numeric",
11 | minute: "numeric" as "numeric",
12 | hour12: true,
13 | };
14 |
15 | const time = new Date(`1970-01-01T${timeString}`);
16 | if (isNaN(time.getTime())) {
17 | console.error("Invalid time string:", timeString);
18 | return "Invalid time";
19 | }
20 |
21 | return new Intl.DateTimeFormat("en-US", options).format(time);
22 | }
23 |
24 | export function roundDistanceToHundreds(distance: number) {
25 | return Math.round(distance * 100) / 100;
26 | }
27 |
28 | export const sliceString = (str: string, maxLength: number) => {
29 | return str.length > maxLength ? str.slice(0, maxLength) + "..." : str;
30 | };
31 |
--------------------------------------------------------------------------------
/frontend/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@radix-ui/react-accordion": "^1.2.0",
13 | "@radix-ui/react-hover-card": "^1.1.1",
14 | "@radix-ui/react-icons": "^1.3.0",
15 | "@radix-ui/react-scroll-area": "^1.1.0",
16 | "class-variance-authority": "^0.7.0",
17 | "clsx": "^2.1.1",
18 | "lucide-react": "^0.445.0",
19 | "mapbox-gl": "^3.7.0",
20 | "next": "14.2.13",
21 | "react": "^18",
22 | "react-dom": "^18",
23 | "tailwind-merge": "^2.5.2",
24 | "tailwindcss-animate": "^1.0.7"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20",
28 | "@types/react": "^18",
29 | "@types/react-dom": "^18",
30 | "eslint": "^8",
31 | "eslint-config-next": "14.2.13",
32 | "postcss": "^8",
33 | "tailwindcss": "^3.4.1",
34 | "typescript": "^5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/frontend/public/images/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaypyles/open-spots/8026c9e00b0cc404da8e4f02cca8b64c340038c7/frontend/public/images/github.png
--------------------------------------------------------------------------------
/frontend/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaypyles/open-spots/8026c9e00b0cc404da8e4f02cca8b64c340038c7/frontend/public/logo.png
--------------------------------------------------------------------------------
/frontend/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | safelist: [
11 | "w-2",
12 | "h-2",
13 | "bg-red-400",
14 | "bg-green-400",
15 | "bg-amber-400",
16 | "shadow-xl",
17 | "shadow-cyan-500/50",
18 | ],
19 | theme: {
20 | extend: {
21 | colors: {
22 | background: "var(--background)",
23 | foreground: "var(--foreground)",
24 | },
25 | borderRadius: {
26 | lg: "var(--radius)",
27 | md: "calc(var(--radius) - 2px)",
28 | sm: "calc(var(--radius) - 4px)",
29 | },
30 | keyframes: {
31 | "accordion-down": {
32 | from: {
33 | height: "0",
34 | },
35 | to: {
36 | height: "var(--radix-accordion-content-height)",
37 | },
38 | },
39 | "accordion-up": {
40 | from: {
41 | height: "var(--radix-accordion-content-height)",
42 | },
43 | to: {
44 | height: "0",
45 | },
46 | },
47 | },
48 | animation: {
49 | "accordion-down": "accordion-down 0.2s ease-out",
50 | "accordion-up": "accordion-up 0.2s ease-out",
51 | },
52 | },
53 | },
54 | plugins: [require("tailwindcss-animate")],
55 | };
56 | export default config;
57 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/frontend/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }]
3 | }
4 |
--------------------------------------------------------------------------------