) => {
const { type, className, children, ...rest } = props;
return (
-
-
- {props.children}
-
+
+
+
+ {props.children}
+
+
);
};
diff --git a/website/components/Faq/styles.module.scss b/website/components/Faq/styles.module.scss
index 8dfb8d28..91878db8 100644
--- a/website/components/Faq/styles.module.scss
+++ b/website/components/Faq/styles.module.scss
@@ -1,9 +1,13 @@
-.faq {
- position: relative;
- .icon {
- display: block;
- position: absolute;
- top: 0.5em;
- right: 0.5em;
+.faqWrapper {
+ margin-bottom: var(--ifm-spacing-vertical);
+ .faq {
+ position: relative;
+ --wrapper-border-left-width: var(--ifm-alert-border-left-width) !important;
+ .icon {
+ display: block;
+ position: absolute;
+ top: 0.5em;
+ right: 0.5em;
+ }
}
}
diff --git a/website/components/Feature/icons.helper.ts b/website/components/Feature/icons.helper.ts
new file mode 100644
index 00000000..a0a4ce4d
--- /dev/null
+++ b/website/components/Feature/icons.helper.ts
@@ -0,0 +1,20 @@
+const CREATIVE_CLOUD_ICON =
+ 'M14.782 3.153c-.231.02-.472.04-.703.07a8.453 8.453 0 0 0-2.832.834 8.951 8.951 0 0 0-2.46 1.777c-.03.04-.09.06-.141.05a7.44 7.44 0 0 0-1.496-.07 7.424 7.424 0 0 0-2.932.763c-1.768.884-3.013 2.26-3.736 4.108a7.089 7.089 0 0 0-.462 2.139c0 .05-.01.09-.02.13v.773c.02.201.05.392.07.593.1.813.332 1.596.703 2.33.824 1.646 2.089 2.851 3.786 3.594a7.127 7.127 0 0 0 2.45.593c.032 0 .06.004.086.01h8.576c.183-.017.362-.035.547-.06a8.344 8.344 0 0 0 2.811-.834 8.836 8.836 0 0 0 3.646-3.304 8.187 8.187 0 0 0 1.184-3.093c.05-.34.08-.692.121-1.034 0-.05.01-.09.02-.13v-.794c-.02-.23-.05-.452-.05-.662a8.345 8.345 0 0 0-.834-2.812 8.952 8.952 0 0 0-3.324-3.645 8.245 8.245 0 0 0-3.072-1.175c-.362-.06-.713-.09-1.075-.13-.05 0-.09-.01-.14-.02zm.369 1.693c2.126.005 3.93.826 5.395 2.455a6.93 6.93 0 0 1 1.616 3.323c.15.764.181 1.547.07 2.32-.19 1.346-.702 2.55-1.576 3.605a7.082 7.082 0 0 1-3.997 2.45 7.297 7.297 0 0 1-2.56.1c-1.095-.14-2.099-.501-3.003-1.154a5.2 5.2 0 0 1-.672-.573c-1.226-1.205-2.44-2.42-3.666-3.625-.301-.3-.321-.632-.18-.934a.822.822 0 0 1 .863-.472c.21.02.372.141.522.292 1.105 1.114 2.2 2.209 3.304 3.324a5.263 5.263 0 0 0 3.093 1.536c1.948.261 3.605-.341 4.92-1.798.713-.793 1.145-1.747 1.326-2.811.26-1.587-.11-3.013-1.095-4.268-.873-1.115-2.018-1.808-3.404-2.059-1.416-.25-2.751.02-3.966.794-.03.02-.1.03-.131.01a9.04 9.04 0 0 0-1.406-.854s-.01-.01-.02-.03a6.603 6.603 0 0 1 1.255-.823 6.646 6.646 0 0 1 2.641-.784 8.45 8.45 0 0 1 .67-.024zM7.546 7.509c1.455-.024 2.791.525 3.982 1.63.854.802 1.637 1.636 2.46 2.47.231.23.281.522.171.833-.11.311-.362.462-.683.512a.722.722 0 0 1-.632-.23c-.784-.784-1.567-1.557-2.34-2.35-.633-.653-1.386-1.025-2.27-1.186-1.958-.351-3.936.784-4.639 2.641-.904 2.36.522 5.031 2.982 5.594.482.11.995.11 1.497.1.14-.01.22.04.32.13.483.473.995.945 1.497 1.416.03.03.07.06.1.09-.06 0-.1.01-.14.01h-2.3a5.833 5.833 0 0 1-5.693-4.568c-.653-2.942 1.034-5.925 3.926-6.798a6.33 6.33 0 0 1 1.762-.294Z';
+import * as MdiIcons from '@mdi/js';
+import { camelCased } from '@tdev-plugins/helpers';
+
+const CustomIcons = {
+ mdiAdobeCreativeCloud: CREATIVE_CLOUD_ICON
+};
+type MdiIconType = keyof typeof MdiIcons | keyof typeof CustomIcons;
+
+export const getIcon = (icon: MdiIconType | string | undefined): string => {
+ if (!icon) {
+ return MdiIcons.mdiFileDocumentOutline;
+ }
+ const ico = camelCased(icon);
+ if (ico in CustomIcons) {
+ return CustomIcons[ico as keyof typeof CustomIcons];
+ }
+ return MdiIcons[ico as keyof typeof MdiIcons] ?? MdiIcons.mdiFileDocumentOutline;
+};
diff --git a/website/components/Feature/index.tsx b/website/components/Feature/index.tsx
index 0f118a43..d51596f3 100644
--- a/website/components/Feature/index.tsx
+++ b/website/components/Feature/index.tsx
@@ -6,6 +6,7 @@ import Icon from '@mdi/react';
import * as MdiIcons from '@mdi/js';
import { camelCased } from '@site/src/plugins/helpers';
import Card from '@tdev-components/shared/Card';
+import { getIcon } from './icons.helper';
export interface FeatureProps {
name: string;
@@ -48,11 +49,7 @@ const Feature = (props: FeatureProps) => {
/>
)}
-
+
{name}
{description && {description}
}
diff --git a/website/components/MailTemplate/index.tsx b/website/components/MailTemplate/index.tsx
new file mode 100644
index 00000000..8a9e0889
--- /dev/null
+++ b/website/components/MailTemplate/index.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import clsx from 'clsx';
+import styles from './styles.module.scss';
+import { observer } from 'mobx-react-lite';
+import { useStore } from '@tdev-hooks/useStore';
+import Button from '@tdev-components/shared/Button';
+import Page from '@tdev-models/Page';
+import { mdiSend } from '@mdi/js';
+import Alert from '@tdev-components/shared/Alert';
+import SolutionStyles from '@tdev-components/documents/Solution/styles.module.scss';
+
+export interface ValidationResult {
+ valid: boolean;
+ message?: string;
+}
+interface Props {
+ to: string;
+ subject?: string | ((page: Page) => string);
+ text?: string;
+ icon?: string;
+ color?: string;
+ body: string | ((page: Page) => string);
+ validate?: (page: Page) => () => ValidationResult;
+}
+
+export const alignLeft = (text: string) =>
+ text
+ .split('\n')
+ .map((line) => line.trimStart())
+ .join('\n');
+
+const MailTemplate = observer((props: Props) => {
+ const pageStore = useStore('pageStore');
+ const { current } = pageStore;
+ if (!current) {
+ return null;
+ }
+ const validate = props.validate ? props.validate(current) : () => ({ valid: true });
+ const validation = validate() as { valid: boolean; message?: string };
+
+ return (
+
+
+ );
+});
+
+export default MailTemplate;
diff --git a/website/components/MailTemplate/styles.module.scss b/website/components/MailTemplate/styles.module.scss
new file mode 100644
index 00000000..90b62a14
--- /dev/null
+++ b/website/components/MailTemplate/styles.module.scss
@@ -0,0 +1,9 @@
+.mailTemplate {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ align-items: center;
+ .alert {
+ flex-grow: 1;
+ }
+}
diff --git a/website/components/MailTemplate/templates/FrageSchuelerausweis.tsx b/website/components/MailTemplate/templates/FrageSchuelerausweis.tsx
new file mode 100644
index 00000000..e67740ff
--- /dev/null
+++ b/website/components/MailTemplate/templates/FrageSchuelerausweis.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import clsx from 'clsx';
+import { observer } from 'mobx-react-lite';
+import { DynamicInput } from '@tdev-components/DynamicValues';
+import MailTemplate, { alignLeft } from '..';
+import { translate } from '@docusaurus/Translate';
+import { validateEmail, validatePhoneNumber } from '../validations';
+import { capitalize } from 'es-toolkit/string';
+
+interface Props {}
+
+const FrageSchuelerausweis = observer((props: Props) => {
+ return (
+
+
+
+
+ {
+ const name = page.dynamicValues.get('name');
+ const klasse = page.dynamicValues.get('klasse');
+ if (!name || !klasse) {
+ return () => ({
+ valid: false,
+ message: translate(
+ { id: 'validation.missing-field' },
+ { field: !name ? 'Name' : 'Klasse' }
+ )
+ });
+ }
+ return () => ({ valid: true });
+ }}
+ body={(page) => {
+ const name = page.dynamicValues.get('name') ?? '';
+ const klasse = page.dynamicValues.get('klasse') ?? '';
+ return alignLeft(`Guten Tag
+
+ Ich habe eine Frage zum Schülerausweis:
+
+
+ Freundlichen Grüsse
+
+ ${name}, ${klasse}`);
+ }}
+ />
+
+ );
+});
+
+export default FrageSchuelerausweis;
diff --git a/website/components/MailTemplate/templates/ResetOfficeAccount.tsx b/website/components/MailTemplate/templates/ResetOfficeAccount.tsx
new file mode 100644
index 00000000..5f1d2a8d
--- /dev/null
+++ b/website/components/MailTemplate/templates/ResetOfficeAccount.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import clsx from 'clsx';
+import { observer } from 'mobx-react-lite';
+import { DynamicInput } from '@tdev-components/DynamicValues';
+import MailTemplate, { alignLeft } from '..';
+import { translate } from '@docusaurus/Translate';
+import { validateEmail, validatePhoneNumber } from '../validations';
+import { capitalize } from 'es-toolkit/string';
+
+const anrede = translate({ id: 'support.edubern.anrede' });
+const name = translate({ id: 'support.edubern.name' });
+
+interface Props {}
+
+const ResetOfficeAccount = observer((props: Props) => {
+ return (
+
+
+
+
+ {
+ const email = page.dynamicValues.get('email');
+ const emailValidation = validateEmail(email);
+ if (!emailValidation.valid) {
+ return () => emailValidation;
+ }
+ const phone = page.dynamicValues.get('phone');
+ return () => validatePhoneNumber(phone);
+ }}
+ body={(page) => {
+ const email = page.dynamicValues.get('email') ?? '';
+ const student = email
+ .split('@')[0]
+ .split('.')
+ .map((p) => capitalize(p))
+ .join(' ');
+ const phone = page.dynamicValues.get('phone');
+ return alignLeft(`Guten Tag ${anrede} ${name}
+
+ Bitte setzen Sie meinen Account zurück:
+
+ Mail: ${email}
+ Telefon: ${phone}
+
+ Besten Dank und freundliche Grüsse
+
+ ${student}`);
+ }}
+ />
+
+ );
+});
+
+export default ResetOfficeAccount;
diff --git a/website/components/MailTemplate/validations.ts b/website/components/MailTemplate/validations.ts
new file mode 100644
index 00000000..c109f0d5
--- /dev/null
+++ b/website/components/MailTemplate/validations.ts
@@ -0,0 +1,32 @@
+import { translate } from '@docusaurus/Translate';
+
+const EMAIL_REGEX = new RegExp(
+ `.+\\..+@(edu\\.)?${translate({ id: 'validation.email.domain' }).replace('.', '\\.')}$`,
+ 'i'
+);
+
+export const validateEmail = (email?: string) => {
+ if (!email) {
+ return { valid: false, message: translate({ id: 'validation.missing-field' }, { field: 'E-Mail' }) };
+ }
+ const isValid = EMAIL_REGEX.test(email);
+ if (isValid) {
+ return { valid: true };
+ }
+ return { valid: false, message: translate({ id: 'validation.email.domain.error' }) };
+};
+
+export const validatePhoneNumber = (phoneNumber?: string) => {
+ if (!phoneNumber) {
+ return {
+ valid: false,
+ message: translate({ id: 'validation.missing-field' }, { field: 'Telefon' })
+ };
+ }
+ const cleaned = phoneNumber.replace(/\s+/g, '');
+ const isValid = /^07\d{8}$/.test(cleaned);
+ if (isValid) {
+ return { valid: true };
+ }
+ return { valid: false, message: translate({ id: 'validation.phone.error' }) };
+};
diff --git a/website/components/TLink/index.tsx b/website/components/TLink/index.tsx
new file mode 100644
index 00000000..005921e5
--- /dev/null
+++ b/website/components/TLink/index.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { translate } from '@docusaurus/Translate';
+
+interface Props {
+ /**
+ * id is the translation key for the href and the link text.
+ */
+ id: string;
+ children?: React.ReactNode;
+ protocol?: 'mailto' | 'tel';
+}
+
+const TLink = (props: Props) => {
+ const { id } = props;
+ const value = translate({ id: id });
+ if (!value) {
+ return props.children || id;
+ }
+ let protocol = props.protocol;
+ if (!protocol) {
+ if (value.includes('@')) {
+ protocol = 'mailto';
+ } else if (value.replace(/\s/g, '').match(/^\+?\d+$/)) {
+ protocol = 'tel';
+ }
+ }
+ let sanitized = value.trim();
+ if (protocol === 'tel') {
+ sanitized = sanitized.replace(/\s/g, '');
+ if (!sanitized.startsWith('+')) {
+ sanitized = '+41' + sanitized.replace(/^0/, '');
+ }
+ }
+ const href = `${protocol}:${sanitized}`;
+ return {props.children || value};
+};
+
+export default TLink;
diff --git a/website/components/documents/Solution/styles.module.scss b/website/components/documents/Solution/styles.module.scss
index 5c4095e0..a8308b29 100644
--- a/website/components/documents/Solution/styles.module.scss
+++ b/website/components/documents/Solution/styles.module.scss
@@ -10,6 +10,7 @@ html[data-theme='dark'] {
position: relative;
--current-alert-padding-vertical: var(--ifm-alert-padding-vertical);
--current-alert-padding-horizontal: var(--ifm-alert-padding-horizontal);
+ --wrapper-border-left-width: var(--ifm-alert-border-left-width);
&:not(.standalone) {
:global(.alert) {
border-top-left-radius: 0;
@@ -21,11 +22,11 @@ html[data-theme='dark'] {
-1 * calc(var(--current-alert-padding-vertical) + var(--ifm-alert-border-left-width))
);
border-width: 0;
- border-left-width: var(--ifm-alert-border-left-width) !important;
+ border-left-width: var(--wrapper-border-left-width) !important;
.wrapper {
:global(.alert) {
margin-left: 0;
- border-left-width: 0 !important;
+ --wrapper-border-left-width: 0;
margin-bottom: 0.25rem;
border-top-left-radius: var(--ifm-alert-border-radius);
border-bottom-left-radius: var(--ifm-alert-border-radius);
diff --git a/website/pages/index.tsx b/website/pages/index.tsx
index 2e015f28..f0964b30 100644
--- a/website/pages/index.tsx
+++ b/website/pages/index.tsx
@@ -43,7 +43,7 @@ const IndexPages: React.ComponentProps[] = [
id: 'main.link.infrastructure.name'
}),
icon: 'mdi-home-circle',
- route: '/infrastructure',
+ route: 'https://ict.gbsl.website/infra/',
description: translate({
message: 'Unterrichtszimmer, Drucker, Infrastruktur',
id: 'main.link.infrastructure.description'