Files
homepage/app/components/Services.tsx

259 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import MuiChip from '@mui/material/Chip';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import {styled} from '@mui/material/styles';
import PermMediaIcon from '@mui/icons-material/PermMedia';
import CodeIcon from '@mui/icons-material/Code';
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
import ServiceList from "~/components/ServiceList";
const items = [
{
icon: <PermMediaIcon/>,
title: 'The Vox Sanctum',
serviceList: [
{
name: "Jellyseerr",
url: "http://jellyseerr.aetoskia.com",
desc: "Summon films and series from the digital void.",
external: true
},
{
name: "Sonarr",
url: "http://sonarr.aetoskia.com",
desc: "Keep the endless chronicles of TV under iron control.",
external: true
},
{
name: "Radarr",
url: "http://radarr.aetoskia.com",
desc: "Command the legions of cinema, enforce cinematic order.",
external: true
},
],
description:
"Behold the archive of visual legends, where the eternal campaigns of film and series march forth in eternal crusade against chaos and forgetfulness.",
},
{
icon: <CodeIcon/>,
title: 'The Forge Conclave',
serviceList: [
{
name: "Gitea",
url: "http://gitea.aetoskia.com",
desc: "Forge and safeguard code like a sacred relic.",
external: true
},
{
name: "Registry",
url: "http://registry.aetoskia.com",
desc: "Monitor core constructs of the digital empire.",
external: true
},
{
name: "Drone",
url: "http://drone.aetoskia.com",
desc: "Automaton architect, building pipelines of perfection.",
external: true
},
],
description:
"The bastion of creation — where code is forged in the fires of discipline, guarded like relics, and deployed with unyielding precision to uphold the empire's might.",
},
{
icon: <MonitorHeartIcon/>,
title: 'The Vigilant Watch',
serviceList: [
{
name: "Portainer",
url: "http://portainer.aetoskia.com",
desc: "Oversee the fleet of containers with unyielding vigilance.",
external: true
},
{
name: "Traefik",
url: "http://traefik.aetoskia.com",
desc: "Marshal your gateways and protect the flow between realms.",
external: true
},
],
description:
"Eyes ever watchful, guarding the realms sanctity — these sentinels oversee the flow of life and command the paths between digital dominions.",
},
];
interface ChipProps {
selected?: boolean;
}
const Chip = styled(MuiChip)<ChipProps>(({theme}) => ({
variants: [
{
props: ({selected}) => !!selected,
style: {
background:
'linear-gradient(to bottom right, hsl(210, 98%, 48%), hsl(210, 98%, 35%))',
color: 'hsl(0, 0%, 100%)',
borderColor: (theme.vars || theme).palette.primary.light,
'& .MuiChip-label': {
color: 'hsl(0, 0%, 100%)',
},
...theme.applyStyles('dark', {
borderColor: (theme.vars || theme).palette.primary.dark,
}),
},
},
],
}));
interface MobileLayoutProps {
selectedItemIndex: number;
handleItemClick: (index: number) => void;
selectedFeature: (typeof items)[0];
}
export function MobileLayout({
selectedItemIndex,
handleItemClick,
selectedFeature,
}: MobileLayoutProps) {
if (!items[selectedItemIndex]) {
return null;
}
return (
<Box
sx={{
display: {xs: 'flex', sm: 'none'},
flexDirection: 'column',
gap: 2,
}}
>
<Box sx={{display: 'flex', gap: 2, overflow: 'auto'}}>
{items.map(({title}, index) => (
<Chip
size="medium"
key={index}
label={title}
onClick={() => handleItemClick(index)}
selected={selectedItemIndex === index}
/>
))}
</Box>
<Card variant="outlined">
<Box sx={{px: 2, pb: 2}}>
<Typography
gutterBottom
sx={{color: 'text.primary', fontWeight: 'medium'}}
>
{selectedFeature.title}
</Typography>
<Typography variant="body2" sx={{color: 'text.secondary', mb: 1.5}}>
{selectedFeature.description}
</Typography>
</Box>
<ServiceList serviceList={selectedFeature.serviceList} />
</Card>
</Box>
);
}
export default function Services() {
const [selectedItemIndex, setSelectedItemIndex] = React.useState(0);
const handleItemClick = (index: number) => {
setSelectedItemIndex(index);
};
const selectedFeature = items[selectedItemIndex];
return (
<Container id="features" sx={{bgcolor: "#1a1a1a", borderRadius: 3, p: 3, boxShadow: 6}}>
<Box
sx={{
display: 'flex',
flexDirection: {xs: 'column', md: 'row-reverse'},
gap: 2,
}}
>
<Box
sx={{
display: {xs: 'none', sm: 'flex'},
flexDirection: 'column',
gap: 2,
height: '100%',
}}
>
{items.map(({icon, title, description}, index) => (
<Box
key={index}
component={Button}
onClick={() => handleItemClick(index)}
sx={[
(theme) => ({
p: 2,
height: '100%',
width: '100%',
'&:hover': {
backgroundColor: (theme.vars || theme).palette.action.hover,
},
}),
selectedItemIndex === index && {
backgroundColor: 'action.selected',
},
]}
>
<Box
sx={[
{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'left',
gap: 1,
textAlign: 'left',
textTransform: 'none',
color: 'text.secondary',
},
selectedItemIndex === index && {
color: 'text.primary',
},
]}
>
<Typography variant="h6">{icon} {title}</Typography>
<Typography variant="body2">{description}</Typography>
</Box>
</Box>
))}
</Box>
<MobileLayout
selectedItemIndex={selectedItemIndex}
handleItemClick={handleItemClick}
selectedFeature={selectedFeature}
/>
<Box
sx={{
display: {xs: 'none', sm: 'flex'},
width: {xs: '100%', md: '70%'},
}}
>
<Card
variant="outlined"
sx={{
display: {xs: 'none', sm: 'flex'},
maxHeight: '50vh',
}}
>
<ServiceList serviceList={selectedFeature.serviceList} />
</Card>
</Box>
</Box>
</Container>
);
}