Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline Mode #210

Merged
merged 29 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2ecd089
App now loads offline
renatodellosso Jul 7, 2024
d75c741
Started offline scouting, added force offline mode
renatodellosso Jul 10, 2024
d326e5f
Comps now save to local storage
renatodellosso Jul 11, 2024
5cba034
Almost got offline loading of the comp page working
renatodellosso Jul 12, 2024
412efcf
More offline stuff
renatodellosso Jul 12, 2024
e35c17e
Fixed modals
renatodellosso Jul 14, 2024
26d79a9
Improved modals and started QR codes
renatodellosso Jul 14, 2024
5d6917f
File upload and download works now
renatodellosso Jul 14, 2024
88dd117
Everything's being weird
renatodellosso Jul 16, 2024
5511871
Modals actually work now. DON'T DEFINE COMPONENTS IN OTHER COMPONENTS…
renatodellosso Jul 17, 2024
c411b0d
Comp merging
renatodellosso Jul 18, 2024
07ce34b
I just spent an hour fixing a bug that didn't exist yesterday
renatodellosso Jul 19, 2024
cb833c0
Assigning scouters, uploading to the server
renatodellosso Jul 21, 2024
5de7d11
Quant scouting works now, reworked how comps are selected
renatodellosso Jul 21, 2024
9973661
Subjective scouting works offline now
renatodellosso Jul 22, 2024
5219195
Fallback loading and use hook works now
renatodellosso Jul 22, 2024
78a64d1
Pit reports... kinda
renatodellosso Jul 23, 2024
87379a0
Pit reports almost work
renatodellosso Jul 23, 2024
b11e1de
Pit scouting works!
renatodellosso Jul 24, 2024
9864b6c
Fixed go to offline scouting button
renatodellosso Jul 24, 2024
b2c0c39
QR codes are done, but I can't test since I don't have a camera
renatodellosso Jul 25, 2024
2e6a411
Bug fixes
renatodellosso Jul 25, 2024
5b1ee78
Merge branch 'main' into offline-mode
renatodellosso Jul 25, 2024
75c7414
Started on stats page
renatodellosso Jul 26, 2024
47199db
Merge branch 'offline-mode' of github.com:Decatur-Robotics/Gearbox in…
renatodellosso Jul 26, 2024
34038cc
Offline comp page loads fast now. We REALLY need to clean the comp pa…
renatodellosso Jul 26, 2024
a67fbfe
Merge branch 'main' into offline-mode
renatodellosso Jul 28, 2024
f3d0016
Merge branch 'main' into offline-mode
renatodellosso Aug 10, 2024
7a81314
Fixed build issues
renatodellosso Aug 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions components/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import { useCurrentSession } from "@/lib/client/useCurrentSession";
import Link from "next/link";
import { ReactNode, useEffect, useState } from "react";
import { BiMenu, BiPlus, BiHome, BiSolidPhone } from "react-icons/bi";
import { IoSunny, IoMoon } from "react-icons/io5";
import { IoSunny, IoMoon, IoCloudOffline } from "react-icons/io5";
import { BsGearFill } from "react-icons/bs";
import ClientAPI from "@/lib/client/ClientAPI";
import Footer from "./Footer";
import { FaDiscord, FaSearch } from "react-icons/fa";
import useCheckMobile from "@/lib/client/useCheckMobile";
import { MdInfo, MdWarning } from "react-icons/md";
import { MdInfo, MdOfflineBolt, MdOfflinePin, MdOfflineShare, MdWarning } from "react-icons/md";
import { RiWifiOffLine } from "react-icons/ri";
import Avatar from "./Avatar";
import Banner, { DiscordBanner } from "./Banner";
import { stat } from "fs";
import useIsOnline from "@/lib/client/useIsOnline";
import { forceOfflineMode } from "@/lib/client/ClientUtils";
import Head from "next/head";

const api = new ClientAPI("gearboxiscool");
Expand All @@ -30,7 +34,8 @@ type ContainerProps = {
export default function Container(props: ContainerProps) {
const { session, status } = useCurrentSession();
const user = session?.user;
const authenticated = status === "authenticated";
const isOnline = useIsOnline();
const authenticated = !forceOfflineMode() && status === "authenticated";

const [loadingTeams, setLoadingTeams] = useState<boolean>(false);
const [accepted, setAccepted] = useState(false);
Expand Down Expand Up @@ -78,7 +83,10 @@ export default function Container(props: ContainerProps) {
setLoadingTeams(true);
let newTeams: Team[] = [];
for (const team of user.teams) {
newTeams.push(await api.findTeamById(team));
const foundTeam = await api.findTeamById(team).catch(() => undefined);
if (!foundTeam)
continue;
newTeams.push(foundTeam);
}
setTeams(newTeams);

Expand All @@ -96,7 +104,13 @@ export default function Container(props: ContainerProps) {
setLoadingSeasons(true);

let newSeasons: Season[] = [];
for (const season of selectedTeam.seasons) {

if (!selectedTeam?.seasons) {
setLoadingSeasons(false);
return;
}

for (const season of selectedTeam?.seasons) {
newSeasons.push(await api.findSeasonById(season));
}

Expand Down Expand Up @@ -159,7 +173,7 @@ export default function Container(props: ContainerProps) {
className="grow bg-base-100"
placeholder="Search an event"
/>
<FaSearch></FaSearch>
<FaSearch />
{eventResults.length > 0 ? (
<div className="absolute -translate-x-5 translate-y-20 sm:translate-y-24 w-1/2 sm:w-1/4 bg-base-300 rounded-b-lg sm:p-2">
<ul>
Expand Down Expand Up @@ -192,23 +206,21 @@ export default function Container(props: ContainerProps) {
borderThickness={2}
/>
</Link>
) : (
// <Link
// href={"/profile"}
// tabIndex={0}
// className="btn btn-ghost btn-circle avatar sm:mr-5"
// >
// <div className="w-10 rounded-full">
// <img src={user?.image} />
// </div>
// </Link>
) : isOnline ? (
<a
href={"/api/auth/signin"}
rel="noopener noreferrer"
target="_blank"
>
<button className="btn btn-primary sm:mr-4">Sign In</button>
</a>
) : (
<Link href="/offline" className="btn btn-ghost flex flex-row sm:mr-4">
<div className="tooltip tooltip-left" data-tip="You are offline.">
<RiWifiOffLine className="text-2xl" />
</div>
<span>Go to offline scouting</span>
</Link>
)}

{/*
Expand Down Expand Up @@ -297,7 +309,7 @@ export default function Container(props: ContainerProps) {
{teams.map((team, index) => {
var initials = team.name
?.split(" ")
.map((section) => section.charAt(0)) ?? "?";
?.map((section) => section.charAt(0)) ?? "?";
var selected = index === selectedTeamIndex;
return (
<button
Expand Down
2 changes: 1 addition & 1 deletion components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default function Footer() {
>
<FaList className="inline mr-1" size={16}/>About Us
</Link>
<div>Version {VERSION}</div>
<div>Version {new Date(+process.env.NEXT_PUBLIC_BUILD_TIME).toISOString()}</div>
</nav>
<div className="max-sm:hidden flex-row flex space-x-4">
<Link
Expand Down
185 changes: 185 additions & 0 deletions components/PitReport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import ClientAPI from "@/lib/client/ClientAPI";
import { useCurrentSession } from "@/lib/client/useCurrentSession";
import { FormLayout, FormElement, BlockElement } from "@/lib/Layout";
import { Pitreport, PitReportData } from "@/lib/Types";
import { useState, useCallback } from "react";
import { FaRobot } from "react-icons/fa";
import Flex from "./Flex";
import Checkbox from "./forms/Checkboxes";
import ImageUpload from "./forms/ImageUpload";
import Card from "./Card";
import { getCompFromLocalStorage, updateCompInLocalStorage } from "@/lib/client/offlineUtils";
import QRCode from "react-qr-code";
import { Analytics } from "@/lib/client/Analytics";

const api = new ClientAPI("gearboxiscool");

export default function PitReportForm(props: { pitReport: Pitreport, layout: FormLayout<PitReportData>, compId?: string,
usersteamNumber?: number, compName?: string, username?: string }) {
const { session } = useCurrentSession();

const [pitreport, setPitreport] = useState(props.pitReport);

const setCallback = useCallback(
(key: any, value: boolean | string | number) => {
setPitreport((old) => {
let copy = structuredClone(old);
//@ts-expect-error
copy.data[key] = value;
return copy;
});
},
[]
);

async function submit() {
// Remove _id from object
const { _id, ...report } = pitreport;

console.log("Submitting pitreport", report);
api.updatePitreport(props.pitReport?._id, {
...report,
submitted: true,
submitter: session?.user?._id
})
.catch((e) => {
console.error("Error submitting pitreport", e);

if (!props.compId || !pitreport._id) return;

updateCompInLocalStorage(props.compId, (comp) => {
if (!pitreport._id) {
console.error("Pitreport has no _id");
return;
}

console.log("Updating pitreport in local storage");

comp.pitReports[pitreport._id] = {
...pitreport,
submitted: true,
submitter: session?.user?._id
};
});
})
.then(() => {
Analytics.pitReportSubmitted(pitreport.teamNumber, props.usersteamNumber ?? -1, props.compName ?? "Unknown", props.username ?? "Unknown");
})
.finally(() => {
location.href = location.href.substring(0, location.href.lastIndexOf("/pit"));
});
}

function getComponent(element: FormElement<PitReportData>, isLastInHeader: boolean) {
const key = element.key as string;

if (element.type === "image")
return <ImageUpload report={pitreport} callback={setCallback} />

if (element.type === "boolean")
return <Checkbox label={element.label ?? element.key as string} dataKey={key} data={pitreport} callback={setCallback}
divider={!isLastInHeader} />

if (element.type === "number")
return (<>
<h1 className="font-semibold text-lg">{element.label}</h1>
<input
value={pitreport.data?.[key]}
onChange={(e) => setCallback(key, e.target.value)}
type="number"
className="input input-bordered"
placeholder={element.label}
/>
</>);

if (element.type === "string")
return (
<textarea
value={pitreport.data?.comments}
className="textarea textarea-primary w-[90%]"
placeholder="Say Something Important..."
onChange={(e) => {
setCallback("comments", e.target.value);
}}
/>
);

const entries = Object.entries(element.type!).map((entry, index) => {
const color = ["primary", "accent", "secondary"][index % 3];

return (
<>
<span>{entry[0]}</span>
<input
type="radio"
className={`radio radio-${color}`}
onChange={() =>
setCallback(key, entry[1] as boolean | string | number)
}
checked={pitreport.data?.[element.key as string] === entry[1]}
/>
</>
);
});

return (<>
<h1 className="font-semibold text-lg">{element.label}</h1>
<div className="grid grid-cols-2 translate-x-6 space-y-1">{entries}</div>
</>);
}

const components = Object.entries(props.layout).map(([header, elements]) => {
const inputs = elements.map((element, index) => {
if (!Array.isArray(element))
return getComponent(element as FormElement<PitReportData>, index === elements.length - 1);

const block = element as BlockElement<PitReportData>;
return block?.map((row) =>
row.map((element, elementIndex) =>
getComponent(element, elementIndex === row.length - 1)
)
);
});

return (
<div key={header}>
<h1 className="font-semibold text-lg">{header}</h1>
<div className="translate-x-10">
{inputs}
</div>
</div>
);
});

return (
<Flex mode="col" className="items-center w-screen h-full space-y-4">
<Card className="w-1/4" coloredTop="bg-accent">
<Flex center={true} mode="col">
<h1 className="text-4xl font-semibold">Pitscouting</h1>
<div className="divider"></div>
<h1 className="font-semibold text-2xl">
<FaRobot className="inline mr-2" size={30}></FaRobot>
Team <span className="text-accent">{pitreport.teamNumber}</span>
</h1>
</Flex>
</Card>
<Card>
{components}
<button className="btn btn-primary " onClick={submit}>
Submit
</button>
</Card>
<Card title="Share while offline">
<div className="w-full flex justify-center">
<QRCode value={JSON.stringify({
pitReport: {
...pitreport,
submitted: true,
submitter: session?.user?._id
}
})} />
</div>
</Card>
</Flex>
);
}
78 changes: 39 additions & 39 deletions components/PwaConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,49 @@ export default function PwaConfig() {
return (
<>
<meta name="application-name" content="Gearbox" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Gearbox" />
<meta name="description" content="Scouting made simple." />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-config" content="/icons/browserconfig.xml" />
<meta name="msapplication-TileColor" content="#2B5797" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="theme-color" content="#000000" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Gearbox" />
<meta name="description" content="Scouting made simple." />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-config" content="/icons/browserconfig.xml" />
<meta name="msapplication-TileColor" content="#2B5797" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="theme-color" content="#000000" />

<link rel="apple-touch-icon" href="/icons/touch-icon-iphone.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/touch-icon-ipad.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/touch-icon-iphone-retina.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/icons/touch-icon-ipad-retina.png" />
<link rel="apple-touch-icon" href="/icons/touch-icon-iphone.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/touch-icon-ipad.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/touch-icon-iphone-retina.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/icons/touch-icon-ipad-retina.png" />

<link rel="icon" type="image/png" sizes="220x204" href="/public/favicon.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
<link rel="icon" type="image/png" sizes="220x204" href="/public/favicon.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />

<meta name="twitter:card" content="summary" />
<meta name="twitter:url" content="https://4026.org" />
<meta name="twitter:title" content="Gearbox" />
<meta name="twitter:description" content="Scouting made simple." />
<meta name="twitter:image" content="https://yourdomain.com/icons/android-chrome-192x192.png" />
<meta name="twitter:creator" content="@4026" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Gearbox" />
<meta property="og:description" content="Scouting made simple." />
<meta property="og:site_name" content="Gearbox" />
<meta property="og:url" content="https://4026.org" />
<meta property="og:image" content="https://4026.org/public/favicon.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:url" content="https://4026.org" />
<meta name="twitter:title" content="Gearbox" />
<meta name="twitter:description" content="Scouting made simple." />
<meta name="twitter:image" content="https://yourdomain.com/icons/android-chrome-192x192.png" />
<meta name="twitter:creator" content="@4026" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Gearbox" />
<meta property="og:description" content="Scouting made simple." />
<meta property="og:site_name" content="Gearbox" />
<meta property="og:url" content="https://4026.org" />
<meta property="og:image" content="https://4026.org/public/favicon.png" />

{/* apple splash screen images */}
{/* <link rel='apple-touch-startup-image' href='/images/apple_splash_2048.png' sizes='2048x2732' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1668.png' sizes='1668x2224' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1536.png' sizes='1536x2048' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1125.png' sizes='1125x2436' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1242.png' sizes='1242x2208' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_750.png' sizes='750x1334' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_640.png' sizes='640x1136' /> */}
{/* apple splash screen images */}
{/* <link rel='apple-touch-startup-image' href='/images/apple_splash_2048.png' sizes='2048x2732' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1668.png' sizes='1668x2224' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1536.png' sizes='1536x2048' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1125.png' sizes='1125x2436' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1242.png' sizes='1242x2208' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_750.png' sizes='750x1334' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_640.png' sizes='640x1136' /> */}
</>
);
}
Loading
Loading