Added Homepage components

This commit is contained in:
Sean Edmunds
2024-05-14 16:34:32 -05:00
parent de64d8335a
commit 722d8e65ec
29 changed files with 1499 additions and 52 deletions

View File

@@ -0,0 +1,65 @@
import { Stack } from '@mantine/core';
import { useEffect, useState } from 'react';
import stackClasses from '../../styles/Stack.module.css';
import {useQueryAPI} from '../../hooks/useAPI';
import Search from './components/Search';
import Header from './components/Header';
import Applications from './components/Applications';
import Bookmarks from './components/Bookmarks';
import SettingsButton from './components/SettingsButton';
import { useAtom } from 'jotai';
import { HomepageFilters } from '../../contexts/HomepageFilters';
export default function Homepage() {
const { data, isFetching } = useQueryAPI({ endpoint: 'api/messages' });
const [displayedGreeting, setDisplayedGreeting] = useState('');
const [homepageFilter, setHomepageFilter] = useAtom<any>(HomepageFilters);
function date() {
const currentDate = new Date();
const dateOptions: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
};
const date = currentDate.toLocaleDateString('en-GB', dateOptions);
return date;
}
function greet(list: any) {
const currentTime = new Date();
const greet = Math.floor(currentTime.getHours() / 6);
const easterEggChance = Math.floor(Math.random() * 100);
let timeBased: any = [];
let easterEggs: any = [];
for (const l of list) {
if (l.easteregg === 'N') {
timeBased = timeBased.concat(l);
} else {
easterEggs = easterEggs.concat(l);
}
}
if (easterEggChance > 10 || easterEggs.length === 0) {
return timeBased[greet].message;
}
return easterEggs[Math.floor(Math.random() * easterEggs.length)].message;
}
useEffect(() => {
if (data) {
setDisplayedGreeting(greet(data));
}
}, [isFetching]);
return (
<Stack className={stackClasses.containerStack}>
<Search />
<Header date={date()} greet={displayedGreeting} />
<Applications />
<Bookmarks />
<SettingsButton />
</Stack>
);
}

View File

@@ -0,0 +1,52 @@
import { Icon } from '@iconify/react';
import { Box, Stack, Text } from '@mantine/core';
import { useState } from 'react';
import stacks from '../../../styles/Stack.module.css';
import texts from '../../../styles/Text.module.css';
function AppItem({ url, icon, name }: any) {
const [textDecoration, setTextDecoration] = useState<string>('none');
return (
<Box
component='a'
href={`https://${url}`}
onMouseOver={() => setTextDecoration('underline')}
onMouseOut={() => setTextDecoration('none')}
className={stacks.appItemBox}
>
<Stack
style={{
height: '64px',
marginRight: '1em',
marginLeft: '1em',
paddingTop: '15px',
}}
>
<Icon icon={`mdi:${icon}`} height={32} width={32} />
</Stack>
<Stack
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flex: 1,
overflow: 'hidden',
gap: 0,
}}
>
<Text className={texts.appName}>{name}</Text>
<Text
className={texts.appUrl}
style={{
textDecoration: textDecoration,
}}
>
{url}
</Text>
</Stack>
</Box>
);
}
export default AppItem;

View File

@@ -0,0 +1,51 @@
import { Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import {useQueryAPI} from '../../../hooks/useAPI';
import texts from '../../../styles/Text.module.css';
import AppItem from './AppItem';
function Applications() {
const { data, isFetching } = useQueryAPI({ endpoint: 'api/apps' });
const [applications, setApplications] = useState([]);
const contentType = localStorage.getItem('contenttype');
useEffect(() => {
if (!isFetching) {
setApplications(data);
}
}, [isFetching]);
return (
<Stack id='apps' style={{ gap: 0 }}>
<Text className={texts.sectionTitle}>Applications</Text>
<Stack
style={{
// background: 'var(--color-background)',
gap: '.25rem',
backgroundSize: 'cover',
borderBottom: '0px solid',
display: 'grid',
gridColumnGap: '5px',
gridRowGap: '5px',
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr 1fr',
gridTemplateRows: '64px',
paddingBottom: 'var(--module-spacing)',
}}
>
{applications?.map((item: any) => {
if (item.category === contentType || contentType === 'All')
return (
<AppItem
key={item.name}
name={item.name}
url={item.url}
icon={item.icon}
category={item.category}
/>
);
})}
</Stack>
</Stack>
);
}
export default Applications;

View File

@@ -0,0 +1,15 @@
import BookmarkDetails from './BookmarkDetails';
function BookmarkGroups(props) {
const links = props.links;
return (
<div id='bookmarkForm'>
<h3>{links?.category}</h3>
{links?.map((links) => (
<BookmarkDetails key={links.name} name={links.name} url={links.url} />
))}
</div>
);
}
export default BookmarkGroups;

View File

@@ -0,0 +1,62 @@
import { Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import {useQueryAPI} from '../../../hooks/useAPI';
import texts from '../../../styles/Text.module.css';
import LinkGroups from './LinkGroups';
function Links() {
const { data: linkData, isFetched: linksIsFetched } = useQueryAPI({
endpoint: 'api/links',
});
const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({
endpoint: 'api/categories',
});
const [bookmarks, setBookmarks] = useState([]);
const [categories, setCategories] = useState([]);
useEffect(() => {
if (linksIsFetched && groupsIsFetched) {
setBookmarks(linkData);
setCategories(groupData);
}
}, [linksIsFetched, groupsIsFetched]);
return (
<>
{groupsIsFetched && linksIsFetched && categories?.length > 0 && (
<Stack id='links' style={{ gap: 0 }}>
<Text className={texts.sectionTitle}>Bookmarks</Text>
<Stack
style={{
display: 'grid',
flexWrap: 'nowrap',
gridColumnGap: '20px',
gridRowGap: '0px',
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
gridTemplateRows: 'auto',
}}
>
{categories.map((category: any) => {
if (
category.column_filter === localStorage.getItem('contenttype') ||
localStorage.getItem('contenttype') === 'All'
) {
return (
<LinkGroups
key={category.id}
category={category.category_name}
links={bookmarks.filter(
(bookmark: any) => bookmark.category === category.category_index,
)}
/>
);
}
})}
</Stack>
</Stack>
)}
</>
);
}
export default Links;

View File

@@ -0,0 +1,74 @@
import { Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import {textClasses} from '../../../styles';
function Header({ greet, date }: any) {
const [time, setTime] = useState(new Date());
function fixDigits(k: any) {
if (k < 10) {
return `0${k}`;
}
return k;
}
useEffect(() => {
setInterval(() => {
const nwDate = new Date();
setTime(nwDate);
}, 1000);
}, []);
return (
<Stack
style={{
borderBottom: '0px solid',
zIndex: '1',
clear: 'both',
display: 'flex',
flexDirection: 'row',
gap: 0,
}}
>
<Stack
style={{
display: 'grid',
width: '100%',
}}
>
<Text className={textClasses.greeting} classNames={textClasses}>{greet}</Text>
</Stack>
<Stack
style={{
display: 'flex',
float: 'right',
justifyContent: 'right',
width: 'auto',
}}
>
<Text className={textClasses.dateTime} classNames={textClasses} style={{ width: 'max-content' }}>
{date}
</Text>
<Text
component='h2'
className={textClasses.dateTime}
classNames={textClasses}
style={{
justifyContent: 'right',
display: 'grid',
}}
>
{`${
time.getHours() === 0
? 12
: time.getHours() > 12
? time.getHours() - 12
: time.getHours()
}:${fixDigits(time.getMinutes())} ${time.getHours() >= 12 ? 'PM' : 'AM'}`}
</Text>
</Stack>
</Stack>
);
}
export default Header;

View File

@@ -0,0 +1,22 @@
import { Stack, Text } from '@mantine/core';
import texts from '../../../styles/Text.module.css';
import LinkItem from './LinkItem';
function LinkGroups({ links, category }: any) {
return (
<Stack
style={{
marginBottom: '0em',
display: 'block',
lineHeight: '1.5rem',
}}
>
<Text className={texts.linkCategory}>{category}</Text>
{links?.map((link: any) => (
<LinkItem key={link.name} link={link} />
))}
</Stack>
);
}
export default LinkGroups;

View File

@@ -0,0 +1,12 @@
import { Text } from '@mantine/core';
import texts from '../../../styles/Text.module.css';
function LinkItem({ link }: any) {
return (
<Text className={texts.linkItem} component='a' href={link.url}>
{link.name}
</Text>
);
}
export default LinkItem;

View File

@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import Messages from '../../../components/Messages';
import useQueryAPI from '../../../hooks/useAPI';
function MessageInfo() {
const messageData = useQueryAPI({ endpoint: 'api/messages' });
const [greetings, setGreetings] = useState([]);
useEffect(() => {
if (!messageData.isFetching) {
setGreetings(messageData.data);
}
}, [messageData.isFetching]);
return (
<>
{greetings && (
<div id='messageForm'>
{greetings.map((greeting, index) => (
<Messages
key={greeting.id}
group={index + 1}
index={greeting.index}
greeting={greeting.message}
easterEgg={greeting.easteregg}
/>
))}
</div>
)}
</>
);
}
export default MessageInfo;

View File

@@ -0,0 +1,12 @@
function Provider(props) {
return (
<tr>
<td>
<a href={props.provider.url}>{props.provider.name}</a>
</td>
<td>{props.provider.prefix}</td>
</tr>
);
}
export default Provider;

View File

@@ -0,0 +1,75 @@
import { Stack, TextInput } from "@mantine/core";
import { textInputClasses } from "../../../styles";
import { useQueryAPI } from "../../../hooks";
import { useState } from "react";
export default function Search() {
const { data: providers } = useQueryAPI({ endpoint: 'api/providers' });
const [providerLabel, setProviderLabel] = useState('Google Search');
const [providerURL, setProviderURL] = useState('https://www.google.com/search?q=');
const handleKeyPress = (item: any) => {
const option = item.target.value.substr(1, item.target.value.indexOf(' ') - 1) || item.target.value.substr(1);
const subtext = item.target.value.substr(2 + option.length);
if (item.key === 'Enter') {
if (item.target.value.startsWith('/')) {
open(`${providerURL}${subtext}`, '_self');
} else if (validURL(item.target.value)) {
if (containsProtocol(item.target.value)) window.location = item.target.value;
else open(`https://${item.target.value}`, '_self');
} else {
open(`${providerURL}${subtext}`, '_self');
}
}
}
function validURL(str: any) {
const pattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$',
'i',
); // fragment locator
return !!pattern.test(str);
}
function containsProtocol(str: any) {
const pattern = /^(https?:\/\/){1}.*/i;
return !!pattern.test(str);
}
return (
<Stack
style={{
backgroundColor: 'transparent',
textAlign: 'center',
position: 'relative',
padding: '.3em',
display: 'inline-block',
gap: 0,
}}
>
<TextInput
className={textInputClasses.keyword}
classNames={textInputClasses}
spellCheck='true'
label={providerLabel}
onChange={(item: any) => {
if(item.target.value.startsWith('/')) {
setProviderLabel(providers?.find((p: any) => p.prefix === item.target.value.split(' ')[0]).name);
setProviderURL(providers?.find((p: any) => p.prefix === item.target.value.split(' ')[0]).url);
} else {
setProviderLabel('Google Search');
setProviderURL('https://www.google.com/search?q=');
}
}}
onKeyUp={(item: any) => {
handleKeyPress(item);
}}
/>
</Stack>
);
}

View File

@@ -0,0 +1,97 @@
import { Button, Menu, NavLink, Select, Stack } from '@mantine/core';
import { useEffect, useState } from 'react';
import { FiMenu, FiMonitor } from 'react-icons/fi';
import {useQueryAPI} from '../../../hooks/useAPI';
import button from '../../../styles/Button.module.css';
import menu from '../../../styles/Menu.module.css';
import navLink from '../../../styles/NavLink.module.css';
import selectClasses from '../../../styles/Select.module.css';
import { useAtom } from 'jotai';
import { HomepageFilters } from '../../../contexts/HomepageFilters';
function SettingsButton() {
const { data, isFetched } = useQueryAPI({ endpoint: 'api/appCategories' });
const [categories, setCategories] = useState([{value: 'All', label: 'All'}]);
const [homepageFilter, setHomepageFilter] = useAtom<object>(HomepageFilters);
const classNames = {
root: navLink.root,
body: navLink.body,
section: navLink.section,
label: navLink.label,
description: navLink.description,
chevron: navLink.chevron,
};
useEffect(() => {
if(data) {
setCategories(data.map((category: any) => {return {value: category.category, label: category.category}}));
}
}, [data, isFetched]);
function runWakeCommand() {
const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
};
fetch('/trigger/api/wakeDakota', requestOptions);
}
return (
<Menu
withinPortal
withArrow
shadow='md'
position='top'
classNames={menu}
styles={{
dropdown: {
backgroundColor: 'var(--color-background)',
},
}}
>
<Menu.Target>
<Button className={button.settingsButton}>
<FiMenu />
</Button>
</Menu.Target>
<Menu.Dropdown
style={{
position: 'absolute',
}}
>
<NavLink
label='Settings'
component='a'
href='/trigger/settings'
classNames={classNames}
/>
<NavLink label='Quick Actions' classNames={classNames}>
<NavLink
label='Wake Dakota'
leftSection={<FiMonitor />}
onClick={() => runWakeCommand()}
classNames={classNames}
/>
</NavLink>
<Select
label='Homepage Filter'
data={categories ?? []}
defaultValue={localStorage.getItem('contenttype')}
withCheckIcon={false}
onChange={(data: any) => {
localStorage.setItem('contenttype', data || 'All');
setHomepageFilter({value: data, label: data});
}}
className={selectClasses.filterSelect}
classNames={selectClasses}
styles={{
label: {
color: 'var(--color-text-pri)',
},
}}
/>
</Menu.Dropdown>
</Menu>
);
}
export default SettingsButton;