Import the predictions app
This is a direct import of
https://github.com/filipkujawa/spartan-predictions, but with the API
key removed.
I also added a BUILD file to make sure that bazel doesn't try to
interpret any files in this directory.
We'll clean up the code and make it build with bazel in future
patches.
Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: Iaadaf031ebc6b1ec9fd33e25f1bbfb9043d1bba3
Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
diff --git a/predictions/src/app.css b/predictions/src/app.css
new file mode 100644
index 0000000..b5c61c9
--- /dev/null
+++ b/predictions/src/app.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/predictions/src/app.d.ts b/predictions/src/app.d.ts
new file mode 100644
index 0000000..8f4d638
--- /dev/null
+++ b/predictions/src/app.d.ts
@@ -0,0 +1,9 @@
+// See https://kit.svelte.dev/docs/types#app
+// for information about these interfaces
+// and what to do when importing types
+declare namespace App {
+ // interface Locals {}
+ // interface PageData {}
+ // interface Error {}
+ // interface Platform {}
+}
diff --git a/predictions/src/app.html b/predictions/src/app.html
new file mode 100644
index 0000000..b589b12
--- /dev/null
+++ b/predictions/src/app.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Spartan Predictions</title>
+ <meta charset="utf-8" />
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
+ <meta name="description" content="Spartan Predictions" />
+ <meta name="viewport" content="width=device-width" />
+ <meta
+ name="google-signin-client_id"
+ content="715898418036-8lm9oo7tnu44hmnh7j5pjfr1r1cp3789.apps.googleusercontent.com"
+ />
+ %sveltekit.head%
+ </head>
+ <!-- Google tag (gtag.js) -->
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-1Q4X7F0XTJ"></script>
+ <script>
+ window.dataLayer = window.dataLayer || [];
+ function gtag(){dataLayer.push(arguments);}
+ gtag('js', new Date());
+
+ gtag('config', 'G-1Q4X7F0XTJ');
+ </script>
+ <div>%sveltekit.body%</div>
+ </body>
+
+</html>
diff --git a/predictions/src/lib/authStore.ts b/predictions/src/lib/authStore.ts
new file mode 100644
index 0000000..a7bb686
--- /dev/null
+++ b/predictions/src/lib/authStore.ts
@@ -0,0 +1,16 @@
+import { writable } from 'svelte/store';
+import type { UserInfo } from 'firebase/auth';
+
+const authStore = writable<{
+ isLoggedIn: boolean;
+ user?: UserInfo | null;
+ firebaseControlled: boolean;
+}>({
+ isLoggedIn: false,
+ firebaseControlled: false
+});
+
+export default {
+ subscribe: authStore.subscribe,
+ set: authStore.set
+};
diff --git a/predictions/src/lib/firestore.js b/predictions/src/lib/firestore.js
new file mode 100644
index 0000000..a2dc580
--- /dev/null
+++ b/predictions/src/lib/firestore.js
@@ -0,0 +1,17 @@
+import { getFirestore } from 'firebase/firestore';
+import { initializeApp } from 'firebase/app';
+import { getAuth } from 'firebase/auth';
+
+const firebaseConfig = {
+ apiKey: '',
+ authDomain: 'spartan-predictions.firebaseapp.com',
+ projectId: 'spartan-predictions',
+ storageBucket: 'spartan-predictions.appspot.com',
+ messagingSenderId: '541826816726',
+ appId: '1:541826816726:web:4eb4bc915fd882c7b6c777',
+ measurementId: 'G-1Q4X7F0XTJ'
+};
+
+export const app = initializeApp(firebaseConfig);
+export const auth = getAuth(app);
+export const db = getFirestore(app);
diff --git a/predictions/src/lib/img/google.webp b/predictions/src/lib/img/google.webp
new file mode 100644
index 0000000..22721c8
--- /dev/null
+++ b/predictions/src/lib/img/google.webp
Binary files differ
diff --git a/predictions/src/lib/img/img.ts b/predictions/src/lib/img/img.ts
new file mode 100644
index 0000000..d919e09
--- /dev/null
+++ b/predictions/src/lib/img/img.ts
@@ -0,0 +1,7 @@
+export { default as google } from './google.webp';
+export { default as left } from './left.png';
+export { default as robot } from './robot_climbing.jpg';
+export { default as spartan } from './spartan.png';
+export { default as predict } from './predict.png';
+export { default as podium } from './podium-transparent.webp';
+export { default as treasure } from './treasure-transparent.webp';
diff --git a/predictions/src/lib/img/left.png b/predictions/src/lib/img/left.png
new file mode 100644
index 0000000..af12138
--- /dev/null
+++ b/predictions/src/lib/img/left.png
Binary files differ
diff --git a/predictions/src/lib/img/podium-transparent.webp b/predictions/src/lib/img/podium-transparent.webp
new file mode 100644
index 0000000..6799578
--- /dev/null
+++ b/predictions/src/lib/img/podium-transparent.webp
Binary files differ
diff --git a/predictions/src/lib/img/predict.png b/predictions/src/lib/img/predict.png
new file mode 100644
index 0000000..a93d365
--- /dev/null
+++ b/predictions/src/lib/img/predict.png
Binary files differ
diff --git a/predictions/src/lib/img/robot_climbing.jpg b/predictions/src/lib/img/robot_climbing.jpg
new file mode 100644
index 0000000..b7ce8ca
--- /dev/null
+++ b/predictions/src/lib/img/robot_climbing.jpg
Binary files differ
diff --git a/predictions/src/lib/img/spartan.png b/predictions/src/lib/img/spartan.png
new file mode 100644
index 0000000..a5749e1
--- /dev/null
+++ b/predictions/src/lib/img/spartan.png
Binary files differ
diff --git a/predictions/src/lib/img/treasure-transparent.webp b/predictions/src/lib/img/treasure-transparent.webp
new file mode 100644
index 0000000..c500e90
--- /dev/null
+++ b/predictions/src/lib/img/treasure-transparent.webp
Binary files differ
diff --git a/predictions/src/lib/store.ts b/predictions/src/lib/store.ts
new file mode 100644
index 0000000..f36760d
--- /dev/null
+++ b/predictions/src/lib/store.ts
@@ -0,0 +1,4 @@
+import { writable } from 'svelte/store';
+
+export const uid = writable('');
+export const username = writable('');
diff --git a/predictions/src/routes/+error.svelte b/predictions/src/routes/+error.svelte
new file mode 100644
index 0000000..a17932d
--- /dev/null
+++ b/predictions/src/routes/+error.svelte
@@ -0,0 +1,16 @@
+<script>
+ import { uid } from '$lib/store';
+ let user_id = $uid;
+</script>
+
+<section class="flex items-center h-full">
+ <div class="container flex flex-col items-center justify-center px-5 mx-auto my-8">
+ <div class="max-w-md text-center">
+ <h2 class="mb-8 font-extrabold text-9xl text-primary">
+ <span class="sr-only" />404
+ </h2>
+ <p class="text-2xl font-semibold md:text-3xl pb-5">Uh Oh! Page not found! Error</p>
+ <a href="/"><button class="btn btn-primary">Back to homepage</button></a>
+ </div>
+ </div>
+</section>
diff --git a/predictions/src/routes/+layout.svelte b/predictions/src/routes/+layout.svelte
new file mode 100644
index 0000000..bf1b55c
--- /dev/null
+++ b/predictions/src/routes/+layout.svelte
@@ -0,0 +1,34 @@
+<script>
+ import '../app.css';
+
+ import { getAuth, onAuthStateChanged } from 'firebase/auth';
+ import authStore from '$lib/authStore';
+ import { uid, username } from '$lib/store';
+ import { onMount } from 'svelte';
+ import { app } from '$lib/firestore';
+ import { goto } from '$app/navigation';
+ import { inject } from '@vercel/analytics';
+
+ onMount(() => {
+ const auth = getAuth(app);
+ onAuthStateChanged(auth, (user) => {
+ if (!user) {
+ goto('/');
+ } else {
+ uid.update((uid) => user.uid);
+ username.update((username) => user.displayName);
+ }
+ authStore.set({
+ isLoggedIn: user !== null,
+ user,
+ firebaseControlled: true
+ });
+ });
+ });
+</script>
+
+<body class="overflow-x-hidden">
+ <div class="min-h-screen bg-white ">
+ <slot />
+ </div>
+</body>
diff --git a/predictions/src/routes/+page.svelte b/predictions/src/routes/+page.svelte
new file mode 100644
index 0000000..5353901
--- /dev/null
+++ b/predictions/src/routes/+page.svelte
@@ -0,0 +1,120 @@
+<script>
+ import { robot, predict, podium, spartan, treasure } from '$lib/img/img';
+</script>
+
+<!--Navbar-->
+<div class="pt-2">
+ <div class="navbar bg-white ">
+ <div class="navbar-start">
+ <a class="btn btn-ghost normal-case text-xl text-black" href="/">Spartan Predictions</a>
+ </div>
+ <div class="navbar-end">
+ <a class="btn btn-primary text-black" href="/login">Get started</a>
+ </div>
+ </div>
+</div>
+
+<!--Hero-->
+<div class="hero min-h-screen">
+ <div class="hero-content flex-col lg:flex-row">
+ <div class="md:pr-36">
+ <h1 class="text-7xl font-bold text-center md:text-left">Predict.<br />Compete.<br />Win.</h1>
+ <p class="py-6 text-center md:text-left hidden md:block">An FRC971 App</p>
+ <a href="/login"
+ ><div class="flex justify-center md:justify-start py-6 md:py-0">
+ <button class="btn btn-primary">Get Started</button>
+ </div></a
+ >
+ </div>
+ <img alt="FRC Match" src={robot} class="max-w-xs md:max-w-2xl rounded-lg shadow-2xl" />
+ </div>
+</div>
+
+<div class="flex flex-col space-y-12 md:space-y-0 md:space-x-24 md:flex-row place-content-center">
+ <div>
+ <h1
+ class="text-8xl font-extrabold text-center text-transparent bg-clip-text bg-gradient-to-r from-yellow-500 to-pink-600"
+ >
+ "WOAH"
+ </h1>
+ <h2 class="text-3xl font-bold text-center pt-5">- Téa Fazio</h2>
+ </div>
+ <div>
+ <h1
+ class="text-8xl font-extrabold text-center text-transparent bg-clip-text bg-gradient-to-r from-green-500 to-blue-600"
+ >
+ "COOL"
+ </h1>
+ <h2 class="text-3xl font-bold text-center pt-5">- Kiran Haywood</h2>
+ </div>
+</div>
+
+<div class="grid pt-52 pb-24 place-items-center">
+ <div class="flex flex-col md:flex-row md:space-x-24 space-y-12 md:space-y-0">
+ <div class="card w-96 bg-base-200 shadow-2xl">
+ <figure><img src={predict} alt="UI Feature" /></figure>
+ <div class="card-body">
+ <h2 class="card-title">Place Predictions on Matches</h2>
+ <p>Speculate on who you think will win the match</p>
+ </div>
+ </div>
+
+ <div class="card w-96 bg-base-200 shadow-2xl">
+ <figure class="bg-white"><img src={podium} alt="UI Feature" /></figure>
+ <div class="card-body">
+ <h2 class="card-title">Climb the Leaderboard</h2>
+ <p>Compete with friends for the top spots on the leaderboard</p>
+ </div>
+ </div>
+
+ <div class="card w-96 bg-base-200 shadow-2xl">
+ <figure class="bg-white"><img src={treasure} alt="UI Feature" /></figure>
+ <div class="card-body">
+ <h2 class="card-title">Win Prizes!</h2>
+ <p>Top competitors will win prizes (TBD)</p>
+ </div>
+ </div>
+ </div>
+</div>
+
+<footer class="footer footer-center pt-10 pb-10 bg-primary text-primary-content">
+ <div>
+ <img width="50" height="50" src={spartan} alt="logo" />
+ <p class="font-bold">
+ Spartan Predictions <br />FRC 971
+ </p>
+ <p>Copyright © 2022 - All right reserved</p>
+ </div>
+ <div>
+ <div class="grid grid-flow-col gap-4">
+ <a href="https://www.instagram.com/frc971/" target="blank"
+ ><svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ fill="currentColor"
+ class="bi bi-instagram"
+ viewBox="0 0 16 16"
+ >
+ <path
+ d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334z"
+ />
+ </svg>
+ </a>
+ <a href="https://www.facebook.com/frc971" target="blank">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ height="16"
+ fill="currentColor"
+ class="bi bi-facebook"
+ viewBox="0 0 16 16"
+ >
+ <path
+ d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"
+ />
+ </svg>
+ </a>
+ </div>
+ </div>
+</footer>
diff --git a/predictions/src/routes/home/+layout.svelte b/predictions/src/routes/home/+layout.svelte
new file mode 100644
index 0000000..bd73e20
--- /dev/null
+++ b/predictions/src/routes/home/+layout.svelte
@@ -0,0 +1,81 @@
+<script lang="ts">
+ import authStore from '../../lib/authStore';
+ import { getAuth, signOut } from 'firebase/auth';
+ import { goto } from '$app/navigation';
+ import { onMount } from 'svelte';
+ import { uid, username } from '$lib/store';
+ import { auth } from '$lib/firestore';
+
+ export let data;
+ const { admin, points } = data;
+ let user_points = points.points;
+
+ let user_id: string;
+ uid.subscribe((value) => {
+ user_id = value;
+ });
+
+ authStore.subscribe(async ({ isLoggedIn, firebaseControlled }) => {
+ if (!isLoggedIn && firebaseControlled) {
+ await goto('/');
+ }
+ });
+
+ async function logOut() {
+ signOut(auth)
+ .then(() => {
+ // Sign-out successful.
+ })
+ .catch((error) => {
+ // An error happened.
+ });
+ }
+
+ function manage() {
+ goto('/home/manage');
+ }
+ function leaderboard() {
+ goto('/home/leaderboard');
+ }
+ function predictions() {
+ const uid = localStorage.getItem('uid');
+ goto('/home/predictions/' + uid);
+ }
+</script>
+
+<body style="">
+ <div class="">
+ <div class="navbar bg-secondary">
+ <div class="navbar-start">
+ <a class="btn btn-ghost normal-case text-xl text-black" href="/home"
+ >Spartan Predictions {user_id}</a
+ >
+ </div>
+ <div class="flex justify-end flex-1 px-2">
+ <div class="flex items-stretch">
+ <div class="dropdown dropdown-end">
+ <!--Comment to ignore warnings-->
+ <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
+ <!-- svelte-ignore a11y-label-has-associated-control -->
+ <label tabindex="0" class="btn btn-ghost rounded-btn">▼</label>
+ <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
+ <!-- svelte-ignore a11y-label-has-associated-control -->
+ <ul
+ tabindex="0"
+ class="menu dropdown-content p-4 shadow bg-base-100 rounded-box w-52 mt-4 space-y-2"
+ >
+ {#if admin}
+ <li><button on:click={manage} class="btn btn-outline">Manage</button></li>
+ {/if}
+ <li><button class="btn btn-outline">Balance: ${user_points}</button></li>
+ <li><button on:click={predictions} class="btn btn-outline">Predictions</button></li>
+ <li><button on:click={leaderboard} class="btn btn-outline">Leaderboard</button></li>
+ <li><button on:click={logOut} class="btn btn-outline">Log Out</button></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <slot />
+ </div></body
+>
diff --git a/predictions/src/routes/home/+layout.ts b/predictions/src/routes/home/+layout.ts
new file mode 100644
index 0000000..0505d8a
--- /dev/null
+++ b/predictions/src/routes/home/+layout.ts
@@ -0,0 +1,33 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { doc, getDoc } from 'firebase/firestore';
+import { uid } from '$lib/store';
+import { get } from 'svelte/store';
+
+/** @type {import('./$types').LayoutServerLoad} */
+export async function load() {
+ let user_id: string;
+ uid.subscribe((value) => {
+ user_id = value;
+ });
+
+ //Query Admin Role
+ const docRef = doc(db, 'roles', 'admin');
+ const adminDoc = await getDoc(docRef);
+
+ let admin = false;
+ if (adminDoc.exists()) {
+ const adminData = adminDoc.data();
+ if (adminData.uid.includes(user_id)) {
+ admin = true;
+ }
+ }
+
+ const pointsRef = doc(db, 'points', user_id);
+ const pointsSnap = await getDoc(pointsRef);
+
+ return {
+ admin: admin,
+ points: pointsSnap.data()
+ };
+}
diff --git a/predictions/src/routes/home/+page.server.ts b/predictions/src/routes/home/+page.server.ts
new file mode 100644
index 0000000..1c019fd
--- /dev/null
+++ b/predictions/src/routes/home/+page.server.ts
@@ -0,0 +1,23 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, getDocs, doc, getDoc } from 'firebase/firestore';
+import type { DocumentData } from 'firebase/firestore';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ //Query Events
+ const querySnapshot = await getDocs(collection(db, 'events'));
+
+ let events: DocumentData[] = [];
+ querySnapshot.forEach((doc) => {
+ events.push(doc.data());
+ });
+
+ if (events) {
+ return {
+ events: events
+ };
+ }
+
+ throw error(404, 'Not found');
+}
diff --git a/predictions/src/routes/home/+page.svelte b/predictions/src/routes/home/+page.svelte
new file mode 100644
index 0000000..b77308e
--- /dev/null
+++ b/predictions/src/routes/home/+page.svelte
@@ -0,0 +1,17 @@
+<script>
+ export let data;
+ const { events } = data;
+</script>
+
+<div class="grid h-screen place-items-center">
+ <div class="md:card md:w-80 md:bg-base-300 md:shadow-xl">
+ <div class="card-body">
+ <h2 class="card-title pb-7">Active Events</h2>
+ {#each events as event}
+ <a class="relative" href="/home/events/{event.id}">
+ <button class="btn btn-wide btn-primary">{event.title}</button>
+ </a>
+ {/each}
+ </div>
+ </div>
+</div>
diff --git "a/predictions/src/routes/home/events/\133slug\135/+page.server.ts" "b/predictions/src/routes/home/events/\133slug\135/+page.server.ts"
new file mode 100644
index 0000000..1450b27
--- /dev/null
+++ "b/predictions/src/routes/home/events/\133slug\135/+page.server.ts"
@@ -0,0 +1,35 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, query, where, getDocs, orderBy } from 'firebase/firestore';
+import type { DocumentData } from 'firebase/firestore';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ //Event ID
+ const slug = params.slug;
+
+ //Query Documents
+ const q = query(
+ collection(db, 'matches'),
+ where('event', '==', slug),
+ where('active', '==', true),
+ orderBy('match_number')
+ );
+ const querySnapshot = await getDocs(q);
+
+ let matches: DocumentData[] = [];
+ let ids: string[] = [];
+ querySnapshot.forEach((doc) => {
+ matches.push(doc.data());
+ ids.push(doc.id);
+ });
+
+ if (matches) {
+ return {
+ matches: matches,
+ match_id: ids
+ };
+ }
+
+ throw error(404, 'Not found');
+}
diff --git "a/predictions/src/routes/home/events/\133slug\135/+page.svelte" "b/predictions/src/routes/home/events/\133slug\135/+page.svelte"
new file mode 100644
index 0000000..d569f10
--- /dev/null
+++ "b/predictions/src/routes/home/events/\133slug\135/+page.svelte"
@@ -0,0 +1,33 @@
+<script>
+ import { page } from '$app/stores';
+
+ export let data;
+ const { matches, match_id } = data;
+ const ten_matches = matches.slice(0, 10);
+</script>
+
+<div class="grid h-screen place-items-center content-start md:content-center">
+ <div class="md:card md:w-200 md:bg-base-300 md:shadow-xl">
+ <div class="card-body">
+ <div class="flex space-x-2 ">
+ <a href="/home"><button class="btn btn-square btn-xs btn-primary">←</button></a>
+ <h2 class="card-title pb-7">Upcoming Matches</h2>
+ </div>
+ {#each ten_matches as match, index}
+ <a class="relative" href="/home/events/{$page.params.slug}/{match_id[index]}">
+ <div class="flex items-center space-x-2">
+ <h3 class="text-black">#{match.match_number}</h3>
+ <div class="btn-group">
+ <button class="btn btn-square btn-error">{match.red_team[0]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[1]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[2]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[0]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[1]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[2]}</button>
+ </div>
+ </div>
+ </a>
+ {/each}
+ </div>
+ </div>
+</div>
diff --git "a/predictions/src/routes/home/events/\133slug\135/\133m_id\135/+page.server.ts" "b/predictions/src/routes/home/events/\133slug\135/\133m_id\135/+page.server.ts"
new file mode 100644
index 0000000..58505c4
--- /dev/null
+++ "b/predictions/src/routes/home/events/\133slug\135/\133m_id\135/+page.server.ts"
@@ -0,0 +1,25 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, addDoc, doc, getDoc } from 'firebase/firestore';
+import { invalid } from '@sveltejs/kit';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ //Match ID
+ const match_id = params.m_id;
+
+ //Query Documents
+ const docRef = doc(db, 'matches', match_id);
+ const docSnap = await getDoc(docRef);
+
+ const match = docSnap.data();
+
+ if (match) {
+ return {
+ match: match,
+ match_id: match_id
+ };
+ }
+
+ throw error(404, 'Not found');
+}
diff --git "a/predictions/src/routes/home/events/\133slug\135/\133m_id\135/+page.svelte" "b/predictions/src/routes/home/events/\133slug\135/\133m_id\135/+page.svelte"
new file mode 100644
index 0000000..2d6f9ec
--- /dev/null
+++ "b/predictions/src/routes/home/events/\133slug\135/\133m_id\135/+page.svelte"
@@ -0,0 +1,172 @@
+<script>
+ import { error } from '@sveltejs/kit';
+ import { db } from '$lib/firestore';
+ import { getDoc, doc, setDoc, updateDoc } from 'firebase/firestore';
+
+ export let data;
+ const { match, match_id } = data;
+
+ let predictionSelect = '';
+ let betInput = '1';
+ const betValues = [100, 500, 1000, 2500, 5000, 10000];
+ $: betValue = betValues[parseInt(betInput)];
+ let selectionError = false;
+ let activeEventError = false;
+ let balanceError = false;
+
+ function goBack() {
+ history.back();
+ }
+
+ async function submitSelection() {
+ if (predictionSelect == '') {
+ selectionError = true;
+ } else {
+ const uid = localStorage.getItem('uid');
+
+ //Check if user has enough credits to place bet
+ const userRef = doc(db, 'points', uid);
+ const userSnap = await getDoc(userRef);
+ const userData = userSnap.data();
+
+ if (userData) {
+ if (userData.points < betValue) {
+ balanceError = true;
+ return;
+ }
+ } else {
+ balanceError = true;
+ return;
+ }
+
+ //Unique doc id
+ const doc_id = match_id + '-' + uid;
+
+ //Fixes issue #3
+ //Checks if the match is still active and stops users
+ //from placing predictions if it is NOT active
+ const docRef = doc(db, 'matches', match_id);
+ const docSnap = await getDoc(docRef);
+
+ if (docSnap.exists()) {
+ const activeBool = docSnap.data().active;
+
+ if (!activeBool) {
+ activeEventError = true;
+ return;
+ }
+ } else {
+ activeEventError = true;
+ return;
+ }
+
+ goBack();
+
+ //Check if user has already placed a prediction
+ // and is only updating/editing it
+ // so the user dosen't spend multiple times on the same bet
+ const pdocref = doc(db, 'predictions', doc_id);
+ const pdocSnap = await getDoc(pdocref);
+
+ if (pdocSnap.exists()) {
+ //USER IS EDITING OLD PREDICTION
+ console.log('exists');
+
+ const pdocData = pdocSnap.data();
+
+ const oldBetValue = pdocData.amount;
+
+ if (oldBetValue > betValue) {
+ //User DECREASED bet amount
+ //refund difference
+ const refundDiff = oldBetValue - betValue;
+
+ await updateDoc(userRef, {
+ points: userData.points + refundDiff
+ });
+ } else if (oldBetValue < betValue) {
+ //User INCREASED bet amount
+ //refund difference
+ const refundDiff = betValue - oldBetValue;
+ await updateDoc(userRef, {
+ points: userData.points - refundDiff
+ });
+ }
+
+ //Overwrite the old prediction doc
+ await setDoc(doc(db, 'predictions', doc_id), {
+ match: match_id,
+ prediction: predictionSelect,
+ amount: betValue,
+ uid: uid
+ });
+ } else {
+ //USER IS CREATING A NEW PREDICTION
+ console.log('no exists');
+
+ //Deduct points from user balance
+ await updateDoc(userRef, {
+ points: userData.points - betValue
+ });
+
+ //Create a new prediction doc
+ await setDoc(doc(db, 'predictions', doc_id), {
+ match: match_id,
+ prediction: predictionSelect,
+ amount: betValue,
+ uid: uid
+ });
+ }
+ }
+ }
+</script>
+
+<div class="grid h-screen place-items-center">
+ <div class="card w-200 bg-base-300 shadow-xl">
+ <div class="card-body">
+ <div class="flex space-x-2 ">
+ <button on:click={goBack} class="btn btn-square btn-xs btn-primary">←</button>
+ <h2 class="card-title pb-7">Match #{match.match_number}</h2>
+ </div>
+ <div class="btn-group">
+ <button class="btn btn-square btn-error">{match.red_team[0]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[1]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[2]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[0]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[1]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[2]}</button>
+ </div>
+ <div class="">
+ <select bind:value={predictionSelect} class="select select-bordered w-full max-w-xs">
+ <option disabled selected>Who will win?</option>
+ <option>Red Alliance</option>
+ <option>Blue Alliance</option>
+ </select>
+ <div class="pt-5">
+ <input
+ bind:value={betInput}
+ type="range"
+ min="0"
+ max="5"
+ class="range range-primary"
+ step="1"
+ />
+ <div class="w-full flex justify-between text-xs px-2">
+ <span>100</span>
+ <span>500</span>
+ <span>1000</span>
+ <span>2500</span>
+ <span>5,000</span>
+ <span>10,000</span>
+ </div>
+ </div>
+ <div class="pt-2">
+ <button on:click={submitSelection} class="btn btn-primary">Submit</button>
+ {#if selectionError}<p>Select an option</p>{/if}
+ {#if activeEventError}<p>Error: This match is not active</p>{/if}
+ {#if balanceError}<p>Error getting user balance or user balance insufficient</p>{/if}
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/predictions/src/routes/home/leaderboard/+page.server.ts b/predictions/src/routes/home/leaderboard/+page.server.ts
new file mode 100644
index 0000000..70c298b
--- /dev/null
+++ b/predictions/src/routes/home/leaderboard/+page.server.ts
@@ -0,0 +1,37 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, getDocs } from 'firebase/firestore';
+import type { DocumentData } from 'firebase/firestore';
+
+const sort_by = (field, reverse, primer) => {
+ const key = primer
+ ? function (x) {
+ return primer(x[field]);
+ }
+ : function (x) {
+ return x[field];
+ };
+
+ reverse = !reverse ? 1 : -1;
+
+ return function (a, b) {
+ return (a = key(a)), (b = key(b)), reverse * ((a > b) - (b > a));
+ };
+};
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ //Query Events
+ const eventsSnapshot = await getDocs(collection(db, 'points'));
+
+ let points: DocumentData[] = [];
+ eventsSnapshot.forEach((doc) => {
+ points.push(doc.data());
+ });
+
+ points = points.sort(sort_by('points', true, parseInt));
+
+ return {
+ points: points
+ };
+}
diff --git a/predictions/src/routes/home/leaderboard/+page.svelte b/predictions/src/routes/home/leaderboard/+page.svelte
new file mode 100644
index 0000000..7facf26
--- /dev/null
+++ b/predictions/src/routes/home/leaderboard/+page.svelte
@@ -0,0 +1,51 @@
+<script>
+ export let data;
+ const { points } = data;
+
+ let current_page = 0;
+ let records_per_page = 10;
+
+ function previous() {
+ if (current_page > 0) {
+ current_page -= records_per_page;
+ }
+ }
+
+ function next() {
+ if (current_page < points.length) {
+ current_page += records_per_page;
+ }
+ }
+</script>
+
+<div class="grid h-screen place-items-center content-start md:content-center">
+ <div class="md:card md:w-200 md:bg-base-300 md:shadow-xl">
+ <div class="card-body">
+ <h2 class="card-title pb-7">Leaderboard</h2>
+ <table class="table w-full">
+ <thead>
+ <tr>
+ <th />
+ <th>Name</th>
+ <th>Points</th>
+ <!--<th>Win %</th>-->
+ </tr>
+ </thead>
+ <tbody>
+ {#each points.slice(current_page, current_page + records_per_page) as entry, index}
+ <tr>
+ <th>{index + 1 + current_page}</th>
+ <th><h1>{entry.username}</h1></th>
+ <th><h1>{entry.points.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}</h1></th>
+ <!--<th><h1>{((entry.wins/entry.total_predictions)*100).toFixed(2)}%</h1></th>-->
+ </tr>
+ {/each}
+ </tbody>
+ </table>
+ <div class="flex space-x-2">
+ <btn class="btn btn-outline btn-md md:btn-wide" on:click={previous}>Previous</btn>
+ <btn class="btn btn-outline btn-md md:btn-wide" on:click={next}>Next</btn>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/predictions/src/routes/home/manage/+page.server.ts b/predictions/src/routes/home/manage/+page.server.ts
new file mode 100644
index 0000000..b139a6e
--- /dev/null
+++ b/predictions/src/routes/home/manage/+page.server.ts
@@ -0,0 +1,48 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, query, where, getDocs, doc, getDoc } from 'firebase/firestore';
+import type { DocumentData } from 'firebase/firestore';
+
+const sort_by = (field, reverse, primer) => {
+ const key = primer
+ ? function (x) {
+ return primer(x[field]);
+ }
+ : function (x) {
+ return x[field];
+ };
+
+ reverse = !reverse ? 1 : -1;
+
+ return function (a, b) {
+ return (a = key(a)), (b = key(b)), reverse * ((a > b) - (b > a));
+ };
+};
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ //Query Events
+ const eventsq = query(collection(db, 'events'));
+ const eventsSnapshot = await getDocs(eventsq);
+
+ let events: DocumentData[] = [];
+ eventsSnapshot.forEach((doc) => {
+ events.push(doc.data());
+ });
+
+ const matchesq = query(collection(db, 'matches'), where('match_finished', '==', false));
+ const matchesSnapshot = await getDocs(matchesq);
+
+ let matches: DocumentData[] = [];
+
+ matchesSnapshot.forEach((doc) => {
+ matches.push(doc.data());
+ });
+
+ matches = matches.sort(sort_by('match_number', false, parseInt));
+
+ return {
+ events: events,
+ matches: matches
+ };
+}
diff --git a/predictions/src/routes/home/manage/+page.svelte b/predictions/src/routes/home/manage/+page.svelte
new file mode 100644
index 0000000..ab3dc92
--- /dev/null
+++ b/predictions/src/routes/home/manage/+page.svelte
@@ -0,0 +1,280 @@
+<script>
+ import { doc, updateDoc, collection, query, where, getDocs, getDoc } from 'firebase/firestore';
+ import { db } from '$lib/firestore';
+ import { onMount } from 'svelte';
+ import { goto } from '$app/navigation';
+
+ export let data;
+ const { events, matches } = data;
+
+ //Pagination
+ let current_page = 1;
+ let records_per_page = 5;
+
+ let filteredMatches = matches;
+
+ function pageUp() {
+ if (current_page < Math.ceil(filteredMatches.length / records_per_page)) {
+ current_page++;
+ }
+ }
+ function pageDown() {
+ if (current_page > 1) {
+ current_page--;
+ }
+ }
+
+ let eventSelect = 'None';
+
+ onMount(() => {
+ if (localStorage.getItem('eventSelect') == null) {
+ localStorage.setItem('eventSelect', 'None');
+ } else {
+ eventSelect = localStorage.getItem('eventSelect');
+ console.log('mount', eventSelect, matches);
+ const eventMatches = matches.filter((match) => match.event === eventSelect);
+ filteredMatches = eventMatches.filter((ematch) => ematch.match_finished != true);
+ console.log(filteredMatches);
+ }
+ });
+
+ function handleEventSelect() {
+ localStorage.setItem('eventSelect', eventSelect);
+ console.log('handle', eventSelect, matches);
+ const eventMatches = matches.filter((match) => match.event === eventSelect);
+ filteredMatches = eventMatches.filter((ematch) => ematch.match_finished != true);
+ console.log(filteredMatches);
+ pagination = 0;
+ }
+
+ async function handleActivate(match_number) {
+ const docID = eventSelect + '-' + match_number;
+ const matchRef = doc(db, 'matches', docID);
+ await updateDoc(matchRef, {
+ active: true
+ });
+ location.reload();
+ }
+
+ async function handleDeactivate(match_number) {
+ const docID = eventSelect + '-' + match_number;
+ const matchRef = doc(db, 'matches', docID);
+ await updateDoc(matchRef, {
+ active: false
+ });
+ location.reload();
+ }
+
+ async function handleResultRed(match_number) {
+ const docID = eventSelect + '-' + match_number;
+ const matchRef = doc(db, 'matches', docID);
+ await updateDoc(matchRef, {
+ match_result: 'Red Alliance'
+ });
+ location.reload();
+ }
+
+ async function handleResultBlue(match_number) {
+ const docID = eventSelect + '-' + match_number;
+ const matchRef = doc(db, 'matches', docID);
+ await updateDoc(matchRef, {
+ match_result: 'Blue Alliance'
+ });
+ location.reload();
+ }
+
+ async function handleSubmit(match_number) {
+ const docID = eventSelect + '-' + match_number;
+ const matchRef = doc(db, 'matches', docID);
+ await updateDoc(matchRef, {
+ match_finished: true
+ });
+
+ //first get correct result
+ const docSnap = await getDoc(matchRef);
+ const match_result = docSnap.data().match_result;
+
+ //get all users who got prediction right/wrong
+ const q = query(collection(db, 'predictions'), where('match', '==', docID));
+ let uid_correct = [];
+ let uid_wrong = [];
+
+ const querySnapshot = await getDocs(q);
+ querySnapshot.forEach((doc) => {
+ if (doc.data().prediction == match_result) {
+ uid_correct.push([doc.data().uid, doc.data().amount]);
+ } else {
+ uid_wrong.push(doc.data().uid);
+ }
+ });
+
+ console.log('giving winner points', uid_correct);
+ //Give correct users 10 points
+ if (uid_correct.length > 0) {
+ for (let i = 0; i < uid_correct.length; i++) {
+ let pointsRef = doc(db, 'points', uid_correct[i][0]);
+ let docSnap = await getDoc(pointsRef);
+ let old_points = docSnap.data().points;
+ let old_wins = docSnap.data().wins;
+ let old_total_predictions = docSnap.data().total_predictions;
+
+ await updateDoc(pointsRef, {
+ points: old_points + uid_correct[i][1] + uid_correct[i][1],
+ wins: old_wins + 1,
+ total_predictions: old_total_predictions + 1
+ });
+ }
+ }
+ //Give wrong users -10 points
+ console.log('giving losser points', uid_wrong);
+ if (uid_wrong.length > 0) {
+ for (let i = 0; i < uid_wrong.length; i++) {
+ let pointsRef = doc(db, 'points', uid_wrong[i]);
+ let docSnap = await getDoc(pointsRef);
+ if (!docSnap.data()) {
+ console.log('no doc data');
+ }
+ let old_losses = docSnap.data().losses;
+ let old_total_predictions = docSnap.data().total_predictions;
+
+ await updateDoc(pointsRef, {
+ losses: old_losses + 1,
+ total_predictions: old_total_predictions + 1
+ });
+ }
+ }
+
+ location.reload();
+ }
+</script>
+
+<div class="grid h-screen place-items-center">
+ <div class="grid h-screen place-items-center">
+ <div class="card w-200 bg-base-300 shadow-xl">
+ <div class="card-body">
+ <div class="flex space-x-2">
+ <h2 class="card-title pb-7">Manage Matches</h2>
+ </div>
+ <div class="flex space-x-10">
+ <select
+ bind:value={eventSelect}
+ on:change={handleEventSelect}
+ class="select select-bordered w-full max-w-xs"
+ >
+ <option disabled selected>Select Event</option>
+ {#each events as event}
+ <option>{event.id}</option>
+ {/each}
+ </select>
+ <button on:click={() => goto('/home/manage/create')} class="btn btn-primary"
+ >Create Match</button
+ >
+ <button on:click={() => goto('/home/manage/import')} class="btn btn-primary"
+ >Import Matches</button
+ >
+ <button on:click={() => goto('/home/manage/help')} class="btn btn-accent">Help</button>
+ </div>
+ <br />
+ <table class="table w-full">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>Teams</th>
+ <th>Active</th>
+ <th>Result (Red Win)</th>
+ <th>Submit (Send Points)</th>
+ </tr>
+ </thead>
+ {#if eventSelect != 'None'}
+ <tbody>
+ {#each filteredMatches.slice((current_page - 1) * records_per_page, current_page * records_per_page) as match}
+ <tr>
+ <th><h3 class="text-black">{match.match_number}</h3></th>
+ <th
+ ><div class="btn-group">
+ <button class="btn btn-square btn-error">{match.red_team[0]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[1]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[2]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[0]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[1]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[2]}</button>
+ </div></th
+ >
+ {#if match.active}
+ <th
+ ><input
+ on:click={() => handleDeactivate(match.match_number)}
+ type="checkbox"
+ checked="checked"
+ class="checkbox"
+ /></th
+ >
+ {:else}
+ <th
+ ><input
+ on:click={() => handleActivate(match.match_number)}
+ type="checkbox"
+ class="checkbox"
+ /></th
+ >
+ {/if}
+ <!--Result-->
+ <th>
+ {#if match.active}
+ <select class="select w-full max-w-xs" disabled>
+ <option>Match still active</option>
+ </select>
+ {:else if match.match_result == 'Red Alliance' || match.match_result == ''}
+ <input
+ on:click={() => handleResultBlue(match.match_number)}
+ type="checkbox"
+ checked="checked"
+ class="checkbox checkbox-error"
+ />
+ {:else}
+ <input
+ on:click={() => handleResultRed(match.match_number)}
+ type="checkbox"
+ class="checkbox checkbox-error"
+ />
+ {/if}
+ </th>
+
+ <!--Submit-->
+
+ <th>
+ {#if match.active}
+ <button
+ on:click={() => handleSubmit(match.match_number)}
+ class="btn btn-primary btn-disabled">Submit</button
+ >
+ {:else}
+ <button
+ on:click={() => handleSubmit(match.match_number)}
+ class="btn btn-primary">Submit</button
+ >
+ {/if}
+ </th>
+ </tr>
+ {/each}
+ </tbody>
+ <br />
+ <div class="flex space-x-2">
+ {#if current_page == 1}
+ <button on:click={pageDown} class="btn btn-outline btn-disabled">Back</button>
+ {:else}
+ <button on:click={pageDown} class="btn btn-outline">Back</button>
+ {/if}
+
+ {#if current_page == Math.ceil(filteredMatches.length / records_per_page)}
+ <button on:click={pageUp} class="btn btn-outline btn-disabled">Next</button>
+ {:else}
+ <button on:click={pageUp} class="btn btn-outline">Next</button>
+ {/if}
+ </div>
+ {/if}
+ </table>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/predictions/src/routes/home/manage/create/+page.server.ts b/predictions/src/routes/home/manage/create/+page.server.ts
new file mode 100644
index 0000000..8cb722f
--- /dev/null
+++ b/predictions/src/routes/home/manage/create/+page.server.ts
@@ -0,0 +1,27 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, query, where, getDocs, doc, getDoc } from 'firebase/firestore';
+import type { DocumentData } from 'firebase/firestore';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ //Query Events
+ const eventsSnapshot = await getDocs(collection(db, 'events'));
+
+ let events: DocumentData[] = [];
+ eventsSnapshot.forEach((doc) => {
+ events.push(doc.data());
+ });
+
+ const matchesSnapshot = await getDocs(collection(db, 'matches'));
+
+ let matches: DocumentData[] = [];
+ matchesSnapshot.forEach((doc) => {
+ matches.push(doc.data());
+ });
+
+ return {
+ events: events,
+ matches: matches
+ };
+}
diff --git a/predictions/src/routes/home/manage/create/+page.svelte b/predictions/src/routes/home/manage/create/+page.svelte
new file mode 100644
index 0000000..f751b99
--- /dev/null
+++ b/predictions/src/routes/home/manage/create/+page.svelte
@@ -0,0 +1,88 @@
+<script>
+ import { doc, setDoc } from 'firebase/firestore';
+ import { db } from '$lib/firestore';
+ import { goto } from '$app/navigation';
+
+ export let data;
+ const { events, matches } = data;
+
+ let eventSelect = '';
+ let matchNumberInput = '';
+ let blue_team = '';
+ let red_team = '';
+
+ async function handleCreate() {
+ const docID = eventSelect + '-' + matchNumberInput;
+ const blue_team_array = blue_team.split(',');
+ const red_team_array = red_team.split(',');
+ console.log(blue_team_array);
+ console.log(red_team_array);
+ await setDoc(doc(db, 'matches', docID), {
+ active: true,
+ event: eventSelect,
+ match_finished: false,
+ match_number: matchNumberInput,
+ match_result: 'Red Alliance',
+ blue_team: blue_team_array,
+ red_team: red_team_array
+ });
+
+ goto('/home/manage');
+ }
+</script>
+
+<div class="grid h-screen place-items-center">
+ <div class="card w-200 bg-base-300 shadow-xl">
+ <div class="card-body">
+ <div class="flex space-x-2">
+ <button on:click={() => goto('/home/manage')} class="btn btn-square btn-xs btn-primary"
+ >←</button
+ >
+ <h2 class="card-title pb-7">Create Match</h2>
+ </div>
+ <div class="flex space-x-2">
+ <div>
+ <h4>Select Event</h4>
+ <select bind:value={eventSelect} class="select select-bordered w-full max-w-xs">
+ <option disabled selected>Event</option>
+ {#each events as event}
+ <option>{event.id}</option>
+ {/each}
+ </select>
+ </div>
+ <div>
+ <h4 class="">Match Number</h4>
+ <div class="">
+ <input
+ bind:value={matchNumberInput}
+ type="text"
+ placeholder="Type here"
+ class="input input-bordered input-primary w-full max-w-xs"
+ />
+ </div>
+ </div>
+ </div>
+ <div>
+ <div class="pt-2">
+ <input
+ bind:value={blue_team}
+ type="text"
+ placeholder="Blue Team | Ex. 971,972,973"
+ class="input input-bordered input-accent w-full max-w-xs"
+ />
+ </div>
+ </div>
+ <div>
+ <div class="pt-2">
+ <input
+ bind:value={red_team}
+ type="text"
+ placeholder="Red Team | Ex. 971,972,973"
+ class="input input-bordered input-error w-full max-w-xs"
+ />
+ </div>
+ </div>
+ <button on:click={handleCreate} class="btn btn-primary">Create</button>
+ </div>
+ </div>
+</div>
diff --git a/predictions/src/routes/home/manage/import/+page.server.ts b/predictions/src/routes/home/manage/import/+page.server.ts
new file mode 100644
index 0000000..e41d789
--- /dev/null
+++ b/predictions/src/routes/home/manage/import/+page.server.ts
@@ -0,0 +1,19 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, query, where, getDocs, doc, getDoc } from 'firebase/firestore';
+import type { DocumentData } from 'firebase/firestore';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ //Query Events
+ const eventsSnapshot = await getDocs(collection(db, 'events'));
+
+ let events: DocumentData[] = [];
+ eventsSnapshot.forEach((doc) => {
+ events.push(doc.data());
+ });
+
+ return {
+ events: events
+ };
+}
diff --git a/predictions/src/routes/home/manage/import/+page.svelte b/predictions/src/routes/home/manage/import/+page.svelte
new file mode 100644
index 0000000..43a2f7b
--- /dev/null
+++ b/predictions/src/routes/home/manage/import/+page.svelte
@@ -0,0 +1,106 @@
+<script>
+ import { goto } from '$app/navigation';
+ import { error } from '@sveltejs/kit';
+ import { doc, setDoc } from 'firebase/firestore';
+ import { db } from '$lib/firestore';
+
+ export let data;
+ const { events } = data;
+
+ let eventSelect = '';
+ let urlInput = '';
+ let errorMessage = '';
+
+ async function importMatches() {
+ if (eventSelect == '' || urlInput == '') {
+ errorMessage = 'Select an event and enter a url';
+ return;
+ } else if (!urlInput.includes('bluealliance.com/event/')) {
+ errorMessage = 'Enter a valid TBA url';
+ return;
+ } else {
+ errorMessage = '';
+ }
+
+ const urlArr = urlInput.split('/');
+ const eventCode = urlArr[urlArr.length - 1];
+ console.log(eventCode);
+
+ const url = 'https://www.thebluealliance.com/api/v3/event/' + eventCode + '/matches';
+
+ let response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ 'X-TBA-Auth-Key': '1JYHN4B6jREvErHpgG6uoaJ3dObX2oVp23QMv8KCfTZ3TFfS3Yw6nPWI1vItZEDk'
+ }
+ });
+ let data = await response.json();
+
+ console.log(data);
+
+ for (let i = 0; i < data.length; i++) {
+ const docID = eventSelect + '-' + data[i].match_number;
+ const blue_team_array_raw = data[i].alliances.blue.team_keys;
+ const blue_team_array = [];
+ for (let b = 0; b < 3; b++) {
+ blue_team_array.push(blue_team_array_raw[b].split('c')[1]);
+ }
+ const red_team_array_raw = data[i].alliances.red.team_keys;
+ const red_team_array = [];
+ for (let r = 0; r < 3; r++) {
+ red_team_array.push(red_team_array_raw[r].split('c')[1]);
+ }
+ console.log(blue_team_array);
+ console.log(red_team_array);
+
+ await setDoc(doc(db, 'matches', docID), {
+ active: true,
+ event: eventSelect,
+ match_finished: false,
+ match_number: data[i].match_number,
+ match_result: 'Red Alliance',
+ blue_team: blue_team_array,
+ red_team: red_team_array
+ });
+
+ goto('/home/manage');
+ }
+ }
+</script>
+
+<div class="grid h-screen place-items-center">
+ <div class="card w-200 bg-base-300 shadow-xl">
+ <div class="card-body">
+ <div class="flex space-x-2">
+ <button on:click={() => goto('/home/manage')} class="btn btn-square btn-xs btn-primary"
+ >←</button
+ >
+ <h2 class="card-title pb-7">Import</h2>
+ </div>
+ <div class="flex space-x-2">
+ <div>
+ <h4>Select Event</h4>
+ <select bind:value={eventSelect} class="select select-bordered w-full max-w-xs">
+ <option disabled selected>Event</option>
+ {#each events as event}
+ <option>{event.id}</option>
+ {/each}
+ </select>
+ </div>
+ <div>
+ <h4>Event URL</h4>
+ <input
+ bind:value={urlInput}
+ type="text"
+ placeholder="Event URL"
+ class="input input-bordered w-full max-w-xs"
+ />
+ </div>
+ </div>
+ <button on:click={importMatches} class="btn btn-active btn-primary">Import</button>
+ {#if errorMessage != ''}
+ <h4>{errorMessage}</h4>
+ {/if}
+ </div>
+ </div>
+</div>
diff --git "a/predictions/src/routes/home/predictions/\133uid\135/+page.server.ts" "b/predictions/src/routes/home/predictions/\133uid\135/+page.server.ts"
new file mode 100644
index 0000000..d2d9356
--- /dev/null
+++ "b/predictions/src/routes/home/predictions/\133uid\135/+page.server.ts"
@@ -0,0 +1,31 @@
+import { error } from '@sveltejs/kit';
+import { db } from '$lib/firestore';
+import { collection, query, where, getDocs, doc, getDoc } from 'firebase/firestore';
+import type { DocumentData } from 'firebase/firestore';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load({ params }) {
+ const uid = params.uid;
+
+ const q = query(collection(db, 'predictions'), where('uid', '==', uid));
+ const querySnapshot = await getDocs(q);
+
+ let predictions: DocumentData[] = [];
+
+ querySnapshot.forEach((doc) => {
+ predictions.push(doc.data());
+ });
+
+ let matches: DocumentData[] = [];
+
+ for (let i = 0; i < predictions.length; i++) {
+ const docRef = doc(db, 'matches', predictions[i].match);
+ const docSnap = await getDoc(docRef);
+ matches.push(docSnap.data());
+ }
+
+ return {
+ predictions: predictions,
+ matches: matches
+ };
+}
diff --git "a/predictions/src/routes/home/predictions/\133uid\135/+page.svelte" "b/predictions/src/routes/home/predictions/\133uid\135/+page.svelte"
new file mode 100644
index 0000000..d1c9089
--- /dev/null
+++ "b/predictions/src/routes/home/predictions/\133uid\135/+page.svelte"
@@ -0,0 +1,40 @@
+<script>
+ export let data;
+ const { predictions, matches } = data;
+</script>
+
+<div class="grid h-screen place-items-center content-start md:content-center">
+ <div class="md:card md:w-200 md:bg-base-300 md:shadow-xl">
+ <div class="card-body">
+ <h2 class="card-title pb-7">Your Predictions</h2>
+ <div class="flex space-x-40">
+ <h2 class="pl-10">Match Info</h2>
+ <h2>You</h2>
+ </div>
+ {#each matches as match, index}
+ <a class="relative" href="">
+ <div class="flex items-center space-x-2">
+ <div class="btn-group">
+ <button class="btn btn-square btn-primary">{match.match_number}</button>
+ <button class="btn btn-square btn-error">{match.red_team[0]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[1]}</button>
+ <button class="btn btn-square btn-error">{match.red_team[2]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[0]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[1]}</button>
+ <button class="btn btn-square btn-accent">{match.blue_team[2]}</button>
+ </div>
+ {#if predictions[index].prediction == 'Blue Alliance'}
+ <button class="btn btn-square btn-accent">
+ {predictions[index].amount}
+ </button>
+ {:else if predictions[index].prediction == 'Red Alliance'}
+ <button class="btn btn-square btn-error">
+ {predictions[index].amount}
+ </button>
+ {/if}
+ </div>
+ </a>
+ {/each}
+ </div>
+ </div>
+</div>
diff --git a/predictions/src/routes/login/+page.svelte b/predictions/src/routes/login/+page.svelte
new file mode 100644
index 0000000..af293bb
--- /dev/null
+++ b/predictions/src/routes/login/+page.svelte
@@ -0,0 +1,65 @@
+<script lang="ts">
+ import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
+ import { doc, getDoc, setDoc } from 'firebase/firestore';
+ import { db } from '$lib/firestore';
+ import { goto } from '$app/navigation';
+ import { google } from '$lib/img/img';
+
+ async function createPointsRecord(uid: string, username: string) {
+ //Check if doc has not been created
+ const docRef = doc(db, 'points', uid);
+ const docSnap = await getDoc(docRef);
+ if (docSnap.exists()) {
+ } else {
+ await setDoc(doc(db, 'points', uid), {
+ uid: uid,
+ points: 1000,
+ total_predictions: 0,
+ wins: 0,
+ losses: 0,
+ username: username
+ });
+ }
+ }
+
+ async function loginWithGoogle() {
+ try {
+ const provider = new GoogleAuthProvider();
+
+ const auth = getAuth();
+ signInWithPopup(auth, provider)
+ .then((result) => {
+ const credential = GoogleAuthProvider.credentialFromResult(result);
+ const token = credential.accessToken;
+ const user = result.user;
+ localStorage.setItem('uid', user.uid);
+ createPointsRecord(user.uid, user.displayName);
+
+ goto('/home');
+ })
+ .catch((error) => {
+ // Handle Errors here.
+ const errorCode = error.code;
+ const errorMessage = error.message;
+ console.log(errorMessage);
+ });
+ } catch (e) {
+ console.log(e);
+ }
+ }
+</script>
+
+<svelte:head>
+ <script src="https://apis.google.com/js/platform.js" async defer></script>
+</svelte:head>
+
+<div class="grid h-screen place-items-center">
+ <div class="">
+ <div class="pt-10">
+ <button on:click={loginWithGoogle} class="btn btn-outline text-black">
+ <img src={google} alt="" width="30" class="pr-2" />
+ Sign in with Google
+ </button>
+ </div>
+ </div>
+</div>