Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 4 additions & 5 deletions frontend/app/components/download-button.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { filesize } from "filesize";
import { PropsWithChildren, useRef } from "react";
import { Button, Stack } from "react-bootstrap";
import { IconType } from "react-icons";

type DownloadButtonProps = {
id?: string;
fileName: string;
url: string;
text: string;
size: number;
faIcon: IconDefinition;
faIcon: IconType;
isPrimary: boolean;
};

Expand All @@ -26,8 +25,8 @@ const DownloadButton = (props: PropsWithChildren<DownloadButtonProps>) => {
ref.current?.click();
}}
>
<Stack direction="vertical" className="mx-3">
<FontAwesomeIcon icon={props.faIcon} size="4x" className="m-2" />
<Stack direction="vertical" className="mx-3 d-flex align-items-center justify-content-center">
<props.faIcon size="4em" className="m-2" />
<span className="fw-bold">{props.text}</span>
{props.size && <span className="small">{filesize(props.size, { locale: "de" })}</span>}
</Stack>
Expand Down
34 changes: 34 additions & 0 deletions frontend/app/components/icon-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PropsWithChildren } from "react";
import { ButtonProps as BootstrapButtonProps, Button } from "react-bootstrap";
import { IconType } from "react-icons";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa6";

interface IconButtonProps extends BootstrapButtonProps {
icon: IconType;
iconPos?: "left" | "right";
}

export const IconButton = (props: PropsWithChildren<IconButtonProps>) => {
const combinedClassName =
`px-4 d-flex align-items-center justify-content-center ${props.className || ""}`.trim();
const { icon, iconPos, ...buttonProps } = props;
return (
<Button {...buttonProps} className={combinedClassName}>
{props.iconPos != "right" && <props.icon size="1em" className="me-2" />}
{props.children}
{props.iconPos == "right" && <props.icon size="1em" className="ms-2" />}
</Button>
);
};

export const IconButtonNext = (props: BootstrapButtonProps) => (
<IconButton variant="primary" {...props} icon={FaChevronRight} iconPos="right">
Weiter
</IconButton>
);

export const IconButtonPrev = (props: BootstrapButtonProps) => (
<IconButton variant="secondary" {...props} icon={FaChevronLeft} iconPos="left">
Zurück
</IconButton>
);
79 changes: 0 additions & 79 deletions frontend/app/components/layout/footer.tsx

This file was deleted.

10 changes: 9 additions & 1 deletion frontend/app/components/link.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import { AnchorHTMLAttributes, PropsWithChildren } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import Obfuscate from "react-obfuscate";

interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
Expand All @@ -8,8 +9,15 @@ interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"
}

export const CondLink = ({ condition, ...props }: PropsWithChildren<LinkProps>) => {
const { title, ...rest } = props;
if (condition) {
return <Link {...props}>{props.children}</Link>;
return title ? (
<OverlayTrigger delay={{ show: 500, hide: 0 }} overlay={<Tooltip>{title}</Tooltip>}>
<Link {...rest}>{props.children}</Link>
</OverlayTrigger>
) : (
<Link {...rest}>{props.children}</Link>
);
}
return <>{props.children}</>;
};
Expand Down
30 changes: 16 additions & 14 deletions frontend/app/components/msg-box.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {
IconDefinition,
faCircleExclamation,
faCircleInfo,
faTriangleExclamation,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PropsWithChildren } from "react";
import { Alert, Stack } from "react-bootstrap";
import { IconType } from "react-icons";
import { FaCircleExclamation, FaCircleInfo, FaTriangleExclamation } from "react-icons/fa6";
import { match } from "ts-pattern";

type MsgBoxType = "error" | "warning" | "info";

Expand All @@ -16,16 +12,22 @@ type MsgBoxProps = {
};

const MsgBox = (props: PropsWithChildren<MsgBoxProps>) => {
const styling: Record<MsgBoxType, { bsVariant: string; faIcon: IconDefinition }> = {
error: { bsVariant: "danger", faIcon: faTriangleExclamation },
warning: { bsVariant: "warning", faIcon: faCircleExclamation },
info: { bsVariant: "info", faIcon: faCircleInfo },
};
const bsVariant: string = match(props.type)
.with("error", () => "danger")
.with("warning", () => "warning")
.with("info", () => "info")
.exhaustive();

const Icon: IconType = match(props.type)
.with("error", () => FaTriangleExclamation)
.with("warning", () => FaCircleExclamation)
.with("info", () => FaCircleInfo)
.exhaustive();

return (
<Alert variant={styling[props.type].bsVariant} className="my-2">
<Alert variant={bsVariant} className="my-2">
<Stack direction="horizontal" gap={3}>
<FontAwesomeIcon icon={styling[props.type].faIcon} size="2x" />
<Icon size="2.5em" />
<div>
{props.children}
{props.trace && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faCookie, faHardDrive } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
"use client";

import Link from "next/link";
import { useState } from "react";
import { Accordion, Button, Col, Modal, Row } from "react-bootstrap";
import { ADMIN_MAIL, GITHUB_LINK } from "../utils/constants";
import Container from "./layout/container";
import { CondLink, CondMailLink } from "./link";
import { FaCookie, FaGithub, FaHardDrive } from "react-icons/fa6";
import { ADMIN_MAIL, GITHUB_LINK } from "../../utils/constants";
import { CondLink, CondMailLink } from "../link";
import Container from "./container";

const Disclaimer = () => {
const [showPrivacy, setShowPrivacy] = useState(false);
Expand Down Expand Up @@ -94,7 +94,7 @@ const Disclaimer = () => {
<Accordion>
<Accordion.Item eventKey="server-side-caching">
<Accordion.Header>
<FontAwesomeIcon icon={faHardDrive} className="me-2" /> Serverseitiges Caching
<FaHardDrive className="me-2" /> Serverseitiges Caching
</Accordion.Header>
<Accordion.Body>
<p>
Expand All @@ -117,7 +117,7 @@ const Disclaimer = () => {

<Accordion.Item eventKey="cookies">
<Accordion.Header>
<FontAwesomeIcon icon={faCookie} className="me-2" />
<FaCookie className="me-2" />
Cookies
</Accordion.Header>
<Accordion.Body>
Expand All @@ -136,7 +136,7 @@ const Disclaimer = () => {

<Accordion.Item eventKey="open-source">
<Accordion.Header>
<FontAwesomeIcon icon={faGithub} className="me-2" /> Open Source Software
<FaGithub className="me-2" /> Open Source Software
</Accordion.Header>
<Accordion.Body>
<p>
Expand Down
75 changes: 75 additions & 0 deletions frontend/app/components/page/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use client";

import Link from "next/link";
import { Col, Row, Stack } from "react-bootstrap";
import { FaArrowUpRightFromSquare, FaEnvelope, FaGithub } from "react-icons/fa6";
import Obfuscate from "react-obfuscate";
import {
ADMIN_MAIL,
GITHUB_LINK,
KAPLAN_LINK,
KAPLAN_WEB_LINK_TARGET,
VERSION_FRONTEND,
} from "../../utils/constants";
import { CondLink } from "../link";
import Container from "./container";
import VersionCheck from "./version-check";

const Footer = () => {
return (
<>
<VersionCheck />
<Container>
<footer className="pt-2 my-3 border-top">
<Row>
<Col
xs={12}
sm={6}
className="d-flex order-sm-1 justify-content-center justify-content-sm-end my-1"
>
<Stack direction="horizontal" gap={4}>
{ADMIN_MAIL && (
<Obfuscate
email={ADMIN_MAIL}
obfuscateChildren={false}
style={{ textDecoration: "none" }}
>
<FaEnvelope size="1em" className="me-1" />
Kontakt
</Obfuscate>
)}
{KAPLAN_LINK && (
<Link
href={KAPLAN_LINK}
className="text-decoration-none"
target={KAPLAN_WEB_LINK_TARGET}
>
<FaArrowUpRightFromSquare size="1em" className="me-1" />
KaPlan&nbsp;Web
</Link>
)}
</Stack>
</Col>
<Col
xs={12}
sm={6}
className="d-flex order-sm-0 justify-content-center justify-content-sm-start my-1"
>
<CondLink
condition={!!GITHUB_LINK}
href={GITHUB_LINK!}
className="text-muted text-decoration-none me-2"
title="KeinPlan auf GitHub"
>
<FaGithub size="1.5em" className="me-1" /> KeinPlan{" "}
{VERSION_FRONTEND ? `v${VERSION_FRONTEND}` : <small>(unbekannte Version)</small>}
</CondLink>
</Col>
</Row>
</footer>
</Container>
</>
);
};

export default Footer;
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { faClipboardQuestion } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FaClipboardQuestion } from "react-icons/fa6";
import Container from "./container";

export const Header = () => {
return (
<Container>
<header className="d-flex align-items-center pb-3 mb-1 border-bottom">
<a href="/" className="d-flex align-items-center text-body-emphasis text-decoration-none">
<FontAwesomeIcon icon={faClipboardQuestion} size="2xl" className="me-3" />
<FaClipboardQuestion size="2.5em" className="me-2" />
<span className="fs-4">
Kein<b>Plan</b>
</span>
Expand Down
18 changes: 18 additions & 0 deletions frontend/app/components/page/hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Container from "./container";

const Hero = () => (
<Container>
<h1>Stundenliste in 1 Minute</h1>
<p className="fs-5 col-md-10">
Erstelle mit nur ein paar Klicks Auflistungen deiner Arbeitszeit auf Basis deiner in{" "}
<em>KaPlan</em> hinterlegten Termine. Lade sie als PDF herunter und sende sie direkt ans
Pfarrbüro.
</p>
<p className="fs-5 col-md-10">
Ein Tool für alle, die <q>kein Plan</q> haben, warum sie manuell Stundenzettel pflegen müssen,
obwohl alle Dienste bereits offiziell und zentral verwaltet werden.
</p>
</Container>
);

export default Hero;
Loading
Loading