This commit is contained in:
centauridesigns committed 2024-09-27 00:00:04 +02:00
1 parent ae19d4c2a0
commit 626c91aec8
21 files changed
+6159

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
+36
View File
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+4
View File
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
+5308
View File
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "uiogaming-infoscreen",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^14.2.13",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.13",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
+133
View File
@@ -0,0 +1,133 @@
import React from "react";
import { useState } from "react";
import { useEffect } from "react";
import eventItems from "../../public/eventItems.json";
interface ReocurringEvent {
name: string;
day: string;
time?: string;
timeDiffMs?: number;
}
interface SpecialEvent {
name: string;
date: string;
time: string;
timeDiffMs?: number;
}
interface Place {
name: string;
closingHours: Map<string, string>;
}
const reocurringEvents: ReocurringEvent[] = eventItems.reocurring_events;
const specialEvents: SpecialEvent[] = eventItems.special_events;
const places: Place[] = eventItems.places.map((place: any) => ({
name: place.name,
closingHours: new Map(place.closing_times.map((time: any) => [time.day, time.time])),
}));
const EventCard: React.FC = () => {
const [upcomingEvents, setUpcomingEvents] = useState<
(ReocurringEvent | SpecialEvent)[]
>([]);
useEffect(() => {
const updateEvents = () => {
const now = new Date();
const eventsWithCountdown = reocurringEvents.map((event) => {
const eventTime = getNextReocurringEvent(event.day, event.time);
const timeDiffMs = eventTime.getTime() - now.getTime();
return { ...event, timeDiffMs };
});
const filteredEvents = eventsWithCountdown.filter(
(event) =>
event.timeDiffMs! > 0 && event.timeDiffMs! <= 48 * 60 * 60 * 1000
);
setUpcomingEvents(eventsWithCountdown);
console.log(eventsWithCountdown);
};
updateEvents();
const intervalId = setInterval(updateEvents, 60 * 1000);
return () => clearInterval(intervalId);
}, []);
return (
<div className="flex flex-col bg-foreground rounded-lg p-3">
<div className="flex flex-col gap-3">
{upcomingEvents.map((event) => (
<div className="flex flex-1 flex-row bg-primary rounded-lg p-3 justify-between space">
<div key={event.name} className="flex flex-row gap-3">
<p className="event-type">Event</p>
<p>{event.name}</p>
</div>
<div key={event.name} className="flex flex-col gap-3">
<p>{getTimeDifference(event.timeDiffMs)}</p>
</div>
</div>
))}
</div>
</div>
);
};
const getNextReocurringEvent = (day: string, time: string): Date => {
const daysOfWeek: { [key: string]: number } = {
Sunday: 0,
Monday: 1,
Tuesday: 2,
Wednesday: 3,
Thursday: 4,
Friday: 5,
Saturday: 6,
};
const now = new Date();
const currentDay = now.getDay();
const targetDay = daysOfWeek[day];
if (!time) {
console.error(`Time is undefined for event on ${day}`);
return now;
}
const [eventHour, eventMinute] = time.split(":").map(Number);
const eventDate = new Date();
eventDate.setHours(eventHour, eventMinute, 0, 0);
const daysUntilEvent = (targetDay + 7 - currentDay) % 7;
eventDate.setDate(eventDate.getDate() + daysUntilEvent);
if (daysUntilEvent === 0 && now > eventDate) {
eventDate.setDate(eventDate.getDate() + 7);
}
return eventDate;
};
const getTimeDifference = (timeDiff: number): string => {
const msInHour = 1000 * 60 * 60;
const msInDay = msInHour * 24;
if (timeDiff < msInDay) {
const hoursLeft = Math.floor(timeDiff / msInHour);
console.log("HOURS LEFT", hoursLeft);
return `in ${hoursLeft} hours`;
} else {
const daysLeft = Math.floor(timeDiff / msInDay);
console.log("DAYS LEFT", daysLeft);
return `in ${daysLeft} days`;
}
};
const getNextSpecialEvent = (date: string, time: string) => {};
export default EventCard;
+31
View File
@@ -0,0 +1,31 @@
import React from "react";
import menuItems from "../../public/menuItems.json";
import MenuSection from "./MenuSection";
function MenuCard() {
return (
<div className="flex flex-1 flex-col bg-foreground rounded-lg p-3 gap-3 h-full">
{/*top container*/}
<div className="flex flex-row flex-1 rounded-lg gap-3">
{/*left-side box, alcohol*/}
<MenuSection title="Alcohol" items={menuItems.Alcohol}></MenuSection>
{/*right-side container containing 2 boxes*/}
<div className="flex flex-1 flex-col rounded-lg gap-3">
{/*top box*/}
<MenuSection title="Soda and Energy Drinks" items={menuItems.Soda}></MenuSection>
{/*bottom box*/}
<div className="flex-1 bg-primary rounded-lg p-3">
<h1 className="text-headers">Info</h1>
</div>
</div>
</div>
{/*bottom container and box*/}
<MenuSection title="Snacks and Sweets" items={menuItems.Food}></MenuSection>
</div>
);
}
export default MenuCard;
+41
View File
@@ -0,0 +1,41 @@
interface MenuItem {
name: string;
price: number;
}
interface MenuSectionProps {
title: string;
items: MenuItem[];
}
const MenuSection: React.FC<MenuSectionProps> = ({ title, items }) => {
return (
<div className="flex flex-1 flex-col rounded-lg gap-3">
{/* top box */}
<div className="flex-1 bg-primary rounded-lg p-3">
<h1 className="text-headers">{title}</h1>
<div className="flex flex-row place-content-between">
{/* item names */}
<div className="flex flex-col gap-0.5">
{items.map((item) => (
<div key={item.name} className="flex flex-row">
<p className="text-headers">{item.name}</p>
</div>
))}
</div>
{/* item prices */}
<div className="flex flex-col gap-0.5">
{items.map((item) => (
<div key={item.name} className="flex flex-row justify-end">
<p className="text-headers text-right">kr {item.price}</p>
</div>
))}
</div>
</div>
</div>
</div>
);
};
export default MenuSection;
+55
View File
@@ -0,0 +1,55 @@
interface TemperatureCardProps {
temperature: number;
precipitation: number;
wind: Map<number, number>;
fiveHourForecast: Map<string, number>;
}
const TemperatureCard: React.FC<TemperatureCardProps> = ({
temperature,
precipitation,
wind,
fiveHourForecast,
}) => {
return (
<div className="flex flex-1 flex-col bg-foreground rounded-lg p-3 gap-3 h-full">
{/* two vertical containers */}
<div className="flex flex-row gap-3">
{/* left container */}
<div className="flex flex-1 flex-col gap-3">
<div className="flex flex-1 flex-col bg-primary rounded-lg gap-3 p-3 content-center place-content-center">
<div className="text-headers text-8xl text-center tmp">
{temperature}°
</div>
</div>
<div className="flex flex-col bg-primary rounded-lg p-3">
<div className="flex flex-row place-content-between">
<div className="text-headers text-p forecasts">
{precipitation + " mm"}
</div>
<div className="text-headers text-p forecasts">
{wind.speed + "(" + wind.gust + ") m/s"}
</div>
</div>
</div>
</div>
{/* right container */}
<div className="flex flex-1 flex-col gap-3">
{fiveHourForecast.map((item, index) => (
<div
key={index}
className="flex flex-1 flex-col bg-primary rounded-lg gap-3 p-3 place-content-center"
>
<div className="flex justify-between content-center">
<div className="text-headers text-p forecasts">{item.time}</div>
<div className="text-headers text-p forecasts">{item.temperature}°</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default TemperatureCard;
+53
View File
@@ -0,0 +1,53 @@
import exp from "constants";
import React from "react";
import { useEffect } from "react";
function TimeCard() {
const [time, setTime] = React.useState<Date>(new Date());
const [date, setDate] = React.useState<Date>(new Date());
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(interval);
}, []);
const formatTime = (date: Date) => {
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
};
const formatDate = (date: Date) => {
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
return `${day}.${month}.${year}`;
}
const day = (date: Date) => {
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
return days[date.getDay()];
}
return (
<div className="flex flex-col bg-foreground rounded-lg p-3">
<div className="flex flex-row gap-3">
<div className="flex flex-1 flex-row bg-primary rounded-lg p-3 justify-evenly items-center">
<p>{formatDate(date)}</p>
<div className="text-headers text-p time">
{formatTime(time)}
</div>
<p>{day(date)}</p>
</div>
</div>
</div>
);
}
export default TimeCard;
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
Binary file not shown.
+67
View File
@@ -0,0 +1,67 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #141218;
--foreground: #261F31;
--primary: #392e49;
--headers: #D64097;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #141218;
--foreground: #261F31;
--primary: #392e49;
--headers: #D64097;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
h1 {
font-family: var(--font-nunito);
font-weight: 600;
}
p {
font-family: var(--font-nunito);
font-weight: 400;
font-size: 0.75rem;
color: white !important;
}
.tmp {
font-family: var(--font-rubik);
font-weight: 600;
color: white !important;
}
.forecasts {
font-family: var(--font-nunito);
font-weight: 400;
color: white !important;
}
.time {
font-family: var(--font-nunito);
font-weight: 800;
color: white !important;
}
.event-type {
font-family: var(--font-nunito);
font-weight: 800;
color: white !important;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
+51
View File
@@ -0,0 +1,51 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { Nunito, Rubik } from "next/font/google"
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
const nunito = Nunito({
weight: ["400", "500", "600", "800"],
style: ["normal", "italic"],
subsets: ["latin"],
variable: "--font-nunito"
})
const rubik = Rubik({
weight: ["400", "500", "600", "800"],
style: ["normal", "italic"],
subsets: ["latin"],
variable: "--font-rubik"
})
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased ${nunito.variable} ${rubik.variable}`}
>
{children}
</body>
</html>
);
}
+155
View File
@@ -0,0 +1,155 @@
"use client";
import next from "next";
import MenuCard from "./components/MenuCard";
import TemperatureCard from "./components/TemperatureCard";
import TimeCard from "./components/TimeCard";
import EventCard from "./components/EventCard";
import { useEffect, useState } from "react";
interface WeatherData {
type: string;
geometry: {
type: string;
coordinates: [number, number, number];
};
properties: {
meta: {
updated_at: string;
units: Record<string, string>;
};
timeseries: Array<{
time: string;
data: {
instant: {
details: {
air_temperature: number;
wind_speed: number;
precipitation_amount: number;
};
};
};
}>;
};
}
interface Forecast {
time: string;
temperature: number;
}
interface Wind {
speed: number;
gust: number;
}
export default function Home() {
const [weatherData, setWeatherData] = useState<WeatherData | null>(null);
const [nearestHour, setnearestHour] = useState<string | null>(null);
const [nextThreeHours, setNextThreeHours] = useState<Forecast[]>([]);
const [currentWind, setCurrentWind] = useState<Wind | null>(null);
const [currentPrecipitation, setCurrentPrecipitation] = useState<
number | null
>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchWeatherData = async () => {
try {
const response = await fetch(
// Majorstuen, Oslo, 48m above sea level
"https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=59.93&lon=10.71&altitude=48"
);
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
setWeatherData(data);
// find the nearest timestamp to the current time
if (data && data.properties && data.properties.timeseries) {
const currentTime = new Date();
const timeseries = data.properties.timeseries;
const nearest = data.properties.timeseries.reduce((prev: { time: string }, curr: { time: string }) => {
const prevTime = new Date(prev.time);
const currTime = new Date(curr.time);
return Math.abs(currTime.getTime() - currentTime.getTime()) <
Math.abs(prevTime.getTime() - currentTime.getTime())
? curr
: prev;
});
setnearestHour(nearest.time);
// find the next five hours
const nearestIndex = timeseries.findIndex(
(t: { time: number }) => t.time === nearest.time
);
const nextFive = timeseries
.slice(nearestIndex + 1, nearestIndex + 4)
.map(
(ts: {
time: number;
data: { instant: { details: { air_temperature: any } } };
}) => {
const date = new Date(ts.time);
const hour = date.getHours().toString().padStart(2, "0");
const temperature = ts.data.instant.details.air_temperature;
return { time: hour, temperature: Math.round(temperature) };
}
);
setNextThreeHours(nextFive);
setCurrentPrecipitation(
nearest.data.next_1_hours.details.precipitation_amount
);
setCurrentWind({
speed: Math.round(nearest.data.instant.details.wind_speed),
gust: nearest.data.instant.details.wind_speed_of_gust,
});
}
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
fetchWeatherData();
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
// flex direction vertical
<div className="w-screen h-screen bg-background flex flex-col p-3 gap-3">
<MenuCard></MenuCard>
<TemperatureCard
temperature={Math.round(
Number(
weatherData.properties.timeseries.find(
(t) => t.time === nearestHour
)?.data.instant.details.air_temperature
)
)}
precipitation={currentPrecipitation ?? 0}
wind={currentWind}
fiveHourForecast={nextThreeHours}
/>
<EventCard></EventCard>
<TimeCard></TimeCard>
<div className="flex-1 bg-green-500"></div>
</div>
);
}
+106
View File
@@ -0,0 +1,106 @@
{
"reocurring_events": [
{
"name": "Board Game Night",
"day": "Thursday",
"time": "19:00"
},
{
"name": "Magic: The Gathering Mondays",
"day": "Monday",
"time": "17:00"
},
{
"name": "School of Smash Wednesdays",
"day": "Wednesday",
"time": "16:30"
},
{
"name": "Movie Night",
"day": "Sunday",
"time": "18:00"
}
],
"special_events": [
{
"name": "SlottsLAN",
"date": "28.09.2024",
"time": "18:00"
},
{
"name": "Gangfest",
"date": "02.11.2024",
"time": "23:00"
}
],
"places": [
{
"name": "McDonalds",
"closing_times" : [
{
"day": "Monday",
"time": "03:00"
},
{
"day": "Tuesday",
"time": "00:00"
},
{
"day": "Wednesday",
"time": "00:00"
},
{
"day": "Thursday",
"time": "03:00"
},
{
"day": "Friday",
"time": "03:00"
},
{
"day": "Saturday",
"time": "03:00"
},
{
"day": "Sunday",
"time": "00:00"
}
]
},
{
"name": "Dominos",
"closing_times" : [
{
"day": "Monday",
"time": "22:00"
},
{
"day": "Tuesday",
"time": "22:00"
},
{
"day": "Wednesday",
"time": "22:00"
},
{
"day": "Thursday",
"time": "22:00"
},
{
"day": "Friday",
"time": "22:00"
},
{
"day": "Saturday",
"time": "22:00"
},
{
"day": "Sunday",
"time": "22:00"
}
]
}
]
}
+34
View File
@@ -0,0 +1,34 @@
{
"Alcohol": [
{ "name": "Frydenlund Pilsner", "price": 34 },
{ "name": "Frydenlund Juicy Ipa", "price": 44 },
{ "name": "Frydenlund Fatøl", "price": 38 },
{ "name": "Aass Fatøl", "price": 38 },
{ "name": "Isbjørn Lite", "price": 35 },
{ "name": "Somersby", "price": 40 },
{ "name": "Halmstad Crush", "price": 50 },
{ "name": "Breezer", "price": 41 },
{ "name": "Smirnoff Ice", "price": 48 }
],
"Soda": [
{ "name": "Soda", "price": 15 },
{ "name": "Red Bull", "price": 23 },
{ "name": "Monster", "price": 30 },
{ "name": "Powerade", "price": 32 },
{ "name": "Water", "price": 15 }
],
"Food": [
{ "name": "Pringles", "price": 13 },
{ "name" : "Smash", "price": 35},
{ "name" : "KitKat", "price": 20},
{ "name" : "Oreo", "price": 8},
{ "name" : "Musli Bar", "price": 8},
{ "name" : "Jelly Beans", "price": 26},
{ "name" : "Milka Chocolate", "price": 23},
{ "name" : "Milka Jaffa", "price": 33},
{ "name" : "Popcorn", "price": 10},
{ "name": "Kvikk Lunsj", "price": 10 }
]
}
+22
View File
@@ -0,0 +1,22 @@
import { headers } from "next/headers";
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
primary: "var(--primary)",
headers: "var(--headers)",
},
},
},
plugins: [],
};
export default config;
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}