Add TV Mode

This commit is contained in:
Gabriel Brown 2024-07-23 11:33:29 -05:00
parent eb44093f09
commit 5a809e5903
12 changed files with 198 additions and 71 deletions

View File

@ -2,7 +2,7 @@ import "~/styles/globals.css";
import { Inter as FontSans } from "next/font/google"; import { Inter as FontSans } from "next/font/google";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import Sign_Out from "~/components/auth/Sign_Out"; import { TVModeProvider } from "~/components/context/TVModeContext";
import { type Metadata } from "next"; import { type Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
@ -43,10 +43,9 @@ export default function RootLayout({
fontSans.variable)} fontSans.variable)}
> >
<SessionProvider> <SessionProvider>
<div className="absolute top-4 right-6"> <TVModeProvider>
<Sign_Out /> {children}
</div> </TVModeProvider>
{children}
</SessionProvider> </SessionProvider>
</body> </body>
</html> </html>

View File

@ -3,7 +3,7 @@ import { auth } from "~/auth";
import No_Session from "~/components/ui/No_Session"; import No_Session from "~/components/ui/No_Session";
import Header from "~/components/ui/Header"; import Header from "~/components/ui/Header";
import { getEmployees } from "~/server/functions"; import { getEmployees } from "~/server/functions";
import TechTable from "~/components/ui/TechTable"; import Tech_Table from "~/components/ui/Tech_Table";
export default async function HomePage() { export default async function HomePage() {
const session = await auth(); const session = await auth();
@ -14,8 +14,8 @@ export default async function HomePage() {
return ( return (
<main className="min-h-screen <main className="min-h-screen
bg-gradient-to-b from-[#111111] to-[#212325]"> bg-gradient-to-b from-[#111111] to-[#212325]">
<Header /> <Header />
<TechTable employees={employees}/> <Tech_Table employees={employees}/>
</main> </main>
); );
} }

View File

@ -0,0 +1,15 @@
import { signIn } from "next-auth/react";
import { Button } from "~/components/ui/shadcn/button";
export default function Sign_In() {
return (
<Button
onClick={() => signIn()}
className="bg-gradient-to-tl from-[#35363F] to=[#24191A] rounded-xl
px-4 py-2 md:py-2.5 font-semibold text-white hover:bg-gradient-to-tr
hover:from-[#35363F] hover:to-[#23242F]"
>
<h1 className="md:text-2xl my-auto font-semibold">Sign In</h1>
</Button>
);
};

View File

@ -0,0 +1,30 @@
import { signOut } from "next-auth/react";
import { useSession } from "next-auth/react";
import Image from "next/image";
import { Button } from "~/components/ui/shadcn/button";
export default function Sign_Out() {
const { data: session } = useSession();
if (!session) {
return <div/>;
} else {
const pfp = session?.user?.image ? session.user.image : "/images/default_user_pfp.png";
return (
<div className="flex flex-row">
<Image src={pfp} alt="" width={35} height={35}
className="rounded-full border-2 border-white m-auto mr-1 md:mr-2
max-w-[25px] md:max-w-[35px]"
/>
<Button onClick={() => signOut()}
className="w-full p-2 rounded-xl text-sm md:text-lg"
//bg-gradient-to-tl from-[#35363F] to=[#24191A]
//hover:bg-gradient-to-tr hover:from-[#35363F] hover:to-[#23242F]"
>
Sign Out
</Button>
</div>
);
}
};
//<User_Avatar session={session} />

View File

@ -10,7 +10,7 @@ export default async function Sign_Out() {
// Add User profile picture next to Sign Out button // Add User profile picture next to Sign Out button
const pfp = session?.user?.image ? session.user.image : "/images/default_user_pfp.png"; const pfp = session?.user?.image ? session.user.image : "/images/default_user_pfp.png";
return ( return (
<form className="w-full flex flex-row pt-2 pr-0 md:pt-4 md:pr-8" <form className="flex flex-row"
action={async () => { action={async () => {
"use server" "use server"
await signOut() await signOut()

View File

@ -0,0 +1,32 @@
"use client";
import React, { createContext, useContext, useState } from 'react';
import type { ReactNode } from 'react';
interface TVModeContextProps {
tvMode: boolean;
toggleTVMode: () => void;
}
const TVModeContext = createContext<TVModeContextProps | undefined>(undefined);
export const TVModeProvider = ({ children }: { children: ReactNode }) => {
const [tvMode, setTVMode] = useState(false);
const toggleTVMode = () => {
setTVMode((prev) => !prev);
};
return (
<TVModeContext.Provider value={{ tvMode, toggleTVMode }}>
{children}
</TVModeContext.Provider>
);
};
export const useTVMode = () => {
const context = useContext(TVModeContext);
if (!context) {
throw new Error('useTVMode must be used within a TVModeProvider');
}
return context;
};

View File

@ -1,20 +1,41 @@
"use client";
import Image from "next/image"; import Image from "next/image";
import Sign_Out from "~/components/auth/client/Sign_Out";
import TV_Toggle from "~/components/ui/TV_Toggle";
import { useTVMode } from "~/components/context/TVModeContext";
export default function Header() { export default function Header() {
return ( const { tvMode } = useTVMode();
<header className="w-full py-2 pt-6 md:py-5"> if (tvMode) {
<div className="flex flex-row items-center text-center return (
sm:justify-center ml-4 sm:ml-0 p-4"> <div className="absolute top-4 right-6">
<Image src="/images/tech_tracker_logo.png" <div className="flex flex-row my-auto items-center pt-2 pr-0 md:pt-4 md:pr-8">
alt="Tech Tracker Logo" width={100} height={100} < TV_Toggle />
className="max-w-[40px] md:max-w-[120px]" </div>
/> </div>
<h1 className="title-text text-sm md:text-4xl lg:text-8xl );
bg-gradient-to-r from-[#bec8e6] via-[#F0EEE4] to-[#FFF8E7] } else {
font-bold pl-2 md:pl-12 text-transparent bg-clip-text"> return (
Tech Tracker <header className="w-full py-2 pt-6 md:py-5">
</h1> <div className="absolute top-4 right-6">
</div> <div className="flex flex-row my-auto items-center pt-2 pr-0 md:pt-4 md:pr-8">
</header> < TV_Toggle />
); < Sign_Out />
</div>
</div>
<div className="flex flex-row items-center text-center
sm:justify-center ml-4 sm:ml-0 p-4">
<Image src="/images/tech_tracker_logo.png"
alt="Tech Tracker Logo" width={100} height={100}
className="max-w-[40px] md:max-w-[120px]"
/>
<h1 className="title-text text-sm md:text-4xl lg:text-8xl
bg-gradient-to-r from-[#bec8e6] via-[#F0EEE4] to-[#FFF8E7]
font-bold pl-2 md:pl-12 text-transparent bg-clip-text">
Tech Tracker
</h1>
</div>
</header>
);
}
}; };

View File

@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import Sign_In from "~/components/auth/Sign_In"; import Sign_In from "~/components/auth/server/Sign_In";
import Header from "~/components/ui/Header"; import Header from "~/components/ui/Header";
export default function No_Session() { export default function No_Session() {

View File

@ -0,0 +1,19 @@
"use client";
import { Switch } from "~/components/ui/shadcn/switch";
import { useTVMode } from "~/components/context/TVModeContext";
import { useSession } from "next-auth/react";
export default function TV_Toggle() {
const { tvMode, toggleTVMode } = useTVMode();
const { data: session } = useSession();
if (!session) return <div/>;
return (
<Switch
checked={tvMode}
onCheckedChange={toggleTVMode}
className="bg-gradient-to-br from-[#595959] to-[#444444]
hover:bg-gradient-to-bl hover:from-[#484848] hover:to-[#333333]
mx-4 mt-2"
/>
);
}

View File

@ -1,7 +1,8 @@
'use client'; "use client";
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Loading from "~/components/ui/Loading"; import Loading from "~/components/ui/Loading";
import { useTVMode } from "~/components/context/TVModeContext";
// Define the Employee interface to match data fetched on the server // Define the Employee interface to match data fetched on the server
interface Employee { interface Employee {
@ -11,8 +12,9 @@ interface Employee {
updatedAt: Date; updatedAt: Date;
} }
export default function TechTable({ employees }: { employees: Employee[] }) { export default function Tech_Table({ employees }: { employees: Employee[] }) {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const { tvMode } = useTVMode();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedIds, setSelectedIds] = useState<number[]>([]); const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [selectAll, setSelectAll] = useState(false); const [selectAll, setSelectAll] = useState(false);
@ -34,7 +36,7 @@ export default function TechTable({ employees }: { employees: Employee[] }) {
alert("You must be signed in to update status."); alert("You must be signed in to update status.");
return; return;
} }
// If no employee is selected and status is not empty
if (selectedIds.length === 0 && employeeStatus.trim() !== '') { if (selectedIds.length === 0 && employeeStatus.trim() !== '') {
const cur_user = employees.find(employee => employee.name === session.user?.name); const cur_user = employees.find(employee => employee.name === session.user?.name);
if (cur_user) { if (cur_user) {
@ -57,6 +59,7 @@ export default function TechTable({ employees }: { employees: Employee[] }) {
body: JSON.stringify({ employeeIds: selectedIds, newStatus: employeeStatus }), body: JSON.stringify({ employeeIds: selectedIds, newStatus: employeeStatus }),
}); });
} }
const updatedEmployees = await fetch_employees(); const updatedEmployees = await fetch_employees();
setEmployeeData(updatedEmployees); setEmployeeData(updatedEmployees);
setSelectedIds([]); setSelectedIds([]);
@ -88,11 +91,9 @@ export default function TechTable({ employees }: { employees: Employee[] }) {
const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
await update_status(); await update_status();
// if key is i then focus text input
} }
}; };
// Format time for display
const formatTime = (timestamp: Date) => { const formatTime = (timestamp: Date) => {
const date = new Date(timestamp); const date = new Date(timestamp);
const time = date.toLocaleTimeString('en-US', { const time = date.toLocaleTimeString('en-US', {
@ -104,41 +105,37 @@ export default function TechTable({ employees }: { employees: Employee[] }) {
return `${time} - ${month} ${day}`; return `${time} - ${month} ${day}`;
}; };
// Loading bar while we wait for auth
useEffect(() => { useEffect(() => {
if (status !== "loading") { if (status !== "loading") {
setLoading(false); setLoading(false);
} }
}, [status]); }, [status]);
// Refresh employee data if needed after state updates
useEffect(() => { useEffect(() => {
setEmployeeData(employees); setEmployeeData(employees);
}, [employees]); }, [employees]);
// Fetch employees from the server every 10 seconds
useEffect(() => { useEffect(() => {
const fetchAndUpdateEmployees = async () => { const fetchAndUpdateEmployees = async () => {
const updatedEmployees = await fetch_employees(); const updatedEmployees = await fetch_employees();
setEmployeeData(updatedEmployees); setEmployeeData(updatedEmployees);
}; };
fetchAndUpdateEmployees()
.catch((error) => { fetchAndUpdateEmployees().catch((error) => {
console.error('Error fetching employees:', error); console.error('Error fetching employees:', error);
}); });
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
(async () => { (async () => {
await fetchAndUpdateEmployees(); await fetchAndUpdateEmployees();
})() })().catch((error) => {
.catch((error) => {
console.error('Error fetching employees:', error); console.error('Error fetching employees:', error);
}); });
}, 10000); // Poll every 10 seconds }, 10000);
return () => clearInterval(intervalId); // Clear interval on component unmount return () => clearInterval(intervalId);
}, [fetch_employees]); }, [fetch_employees]);
// Handle checkbox changes
useEffect(() => { useEffect(() => {
if (selectedIds.length === employeeData.length && employeeData.length > 0) { if (selectedIds.length === employeeData.length && employeeData.length > 0) {
setSelectAll(true); setSelectAll(true);
@ -150,10 +147,10 @@ export default function TechTable({ employees }: { employees: Employee[] }) {
if (loading) return <Loading interval_amount={3} />; if (loading) return <Loading interval_amount={3} />;
else { else {
return ( return (
<div> <div className={`techtable-wrapper ${tvMode ? 'content-fullscreen' : ''}`}>
<table className="techtable rounded-2xl w-5/6 m-auto text-center text-[42px]"> <table className={`techtable rounded-2xl m-auto text-center text-[42px] ${tvMode ? 'techtable-fullscreen' : 'w-5/6'}`}>
<thead className="tabletitles border border-[#3e4446] <thead className="tabletitles border border-[#3e4446] bg-gradient-to-b
bg-gradient-to-b from-[#282828] to-[#383838] text-[48px]"> from-[#282828] to-[#383838] text-[48px]">
<tr> <tr>
<th className="py-3 px-4 border border-[#3e4446]"> <th className="py-3 px-4 border border-[#3e4446]">
<input <input
@ -170,8 +167,9 @@ export default function TechTable({ employees }: { employees: Employee[] }) {
</thead> </thead>
<tbody> <tbody>
{employeeData.map((employee) => ( {employeeData.map((employee) => (
<tr className="even:bg-gradient-to-br from-[#272727] to-[#313131] <tr
odd:bg-gradient-to-bl odd:from-[#252525] odd:to-[#212125]" className="even:bg-gradient-to-br from-[#272727] to-[#313131]
odd:bg-gradient-to-bl odd:from-[#252525] odd:to-[#212125]"
key={employee.id} key={employee.id}
> >
<td className="p-1 border border-[#3e4446]"> <td className="p-1 border border-[#3e4446]">
@ -195,28 +193,30 @@ export default function TechTable({ employees }: { employees: Employee[] }) {
))} ))}
</tbody> </tbody>
</table> </table>
<div className="m-auto flex flex-row items-center justify-center py-5"> {!tvMode && (
<input <div className="m-auto flex flex-row items-center justify-center py-5">
autoFocus <input
type="text" autoFocus
placeholder="New Status" type="text"
className="min-w-[120px] lg:min-w-[400px] bg-[#F9F6EE] placeholder="New Status"
py-2 px-3 border-none rounded-xl text-[#111111] lg:text-2xl" className="min-w-[120px] lg:min-w-[400px] bg-[#F9F6EE] py-2 px-3
value={employeeStatus} border-none rounded-xl text-[#111111] lg:text-2xl"
onChange={handleStatusChange} value={employeeStatus}
onKeyDown={handleKeyDown} onChange={handleStatusChange}
/> onKeyDown={handleKeyDown}
<button />
type="submit" <button
className="min-w-[100px] lg:min-w-[160px] m-2 p-2 border-none type="submit"
rounded-xl text-center font-semibold lg:text-2xl hover:text-slate-300 className="min-w-[100px] lg:min-w-[160px] m-2 p-2 border-none rounded-xl
hover:bg-gradient-to-bl hover:from-[#484848] hover:to-[#333333] text-center font-semibold lg:text-2xl hover:text-slate-300
bg-gradient-to-br from-[#595959] to-[#444444]" hover:bg-gradient-to-bl hover:from-[#484848] hover:to-[#333333]
onClick={update_status} bg-gradient-to-br from-[#595959] to-[#444444]"
> onClick={update_status}
Update >
</button> Update
</div> </button>
</div>
)}
</div> </div>
); );
} }

View File

@ -119,3 +119,14 @@
font-size:18px; font-size:18px;
} }
} }
.content-fullscreen {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.techtable-fullscreen {
width: 100%;
height: 100%;
}