diff --git a/app/assets/icons/ic-info.svg b/app/assets/icons/ic-info.svg
new file mode 100644
index 000000000..47ea73219
--- /dev/null
+++ b/app/assets/icons/ic-info.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx
new file mode 100644
index 000000000..4a383edda
--- /dev/null
+++ b/app/assets/javascripts/components/Button.tsx
@@ -0,0 +1,28 @@
+import { FunctionComponent } from 'preact';
+
+const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content cursor-pointer`;
+
+const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \
+focus:bg-contrast hover:bg-contrast`;
+const primaryClass = `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 \
+focus:brightness-130`;
+
+export const Button: FunctionComponent<{
+ className?: string;
+ type: 'normal' | 'primary';
+ label: string;
+ onClick: () => void;
+}> = ({ type, label, className = '', onClick }) => {
+ const buttonClass = type === 'primary' ? primaryClass : normalClass;
+ return (
+
+ );
+};
diff --git a/app/assets/javascripts/components/CircleProgress.tsx b/app/assets/javascripts/components/CircleProgress.tsx
new file mode 100644
index 000000000..16fa917dc
--- /dev/null
+++ b/app/assets/javascripts/components/CircleProgress.tsx
@@ -0,0 +1,38 @@
+import { FunctionComponent } from 'preact';
+
+export const CircleProgress: FunctionComponent<{
+ percent: number;
+ className?: string;
+}> = ({ percent, className = '' }) => {
+ const size = 16;
+ const ratioStrokeRadius = 0.25;
+ const outerRadius = size / 2;
+
+ const radius = outerRadius * (1 - ratioStrokeRadius);
+ const stroke = outerRadius - radius;
+
+ const circumference = radius * 2 * Math.PI;
+ const offset = circumference - (percent / 100) * circumference;
+
+ const transition = `transition: 0.35s stroke-dashoffset;`;
+ const transform = `transform: rotate(-90deg);`;
+ const transformOrigin = `transform-origin: 50% 50%;`;
+ const dasharray = `stroke-dasharray: ${circumference} ${circumference};`;
+ const dashoffset = `stroke-dashoffset: ${offset};`;
+ const style = `${transition} ${transform} ${transformOrigin} ${dasharray} ${dashoffset}`;
+ return (
+
+
+
+ );
+};
diff --git a/app/assets/javascripts/components/CircleProgressTime.tsx b/app/assets/javascripts/components/CircleProgressTime.tsx
new file mode 100644
index 000000000..1f7b6b7d9
--- /dev/null
+++ b/app/assets/javascripts/components/CircleProgressTime.tsx
@@ -0,0 +1,27 @@
+import { FunctionalComponent } from 'preact';
+import { useEffect, useState } from 'preact/hooks';
+import { CircleProgress } from './CircleProgress';
+
+/**
+ * Circular progress bar which runs in a specified time interval
+ * @param time - time interval in ms
+ */
+export const CircleProgressTime: FunctionalComponent<{ time: number }> = ({
+ time,
+}) => {
+ const [percent, setPercent] = useState(0);
+ const interval = time / 100;
+ useEffect(() => {
+ const tick = setInterval(() => {
+ if (percent === 100) {
+ setPercent(0);
+ } else {
+ setPercent(percent + 1);
+ }
+ }, interval);
+ return () => {
+ clearInterval(tick);
+ };
+ });
+ return ;
+};
diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx
index 860b6b362..f649ab38f 100644
--- a/app/assets/javascripts/components/DecoratedInput.tsx
+++ b/app/assets/javascripts/components/DecoratedInput.tsx
@@ -26,12 +26,12 @@ export const DecoratedInput: FunctionalComponent = ({
const classes = `${base} ${stateClasses} ${className}`;
return (
-
+
{left}
diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx
index c6f9f632c..1a30ee761 100644
--- a/app/assets/javascripts/components/Icon.tsx
+++ b/app/assets/javascripts/components/Icon.tsx
@@ -25,6 +25,7 @@ import ThemesIcon from '../../icons/ic-themes.svg';
import UserIcon from '../../icons/ic-user.svg';
import CopyIcon from '../../icons/ic-copy.svg';
import DownloadIcon from '../../icons/ic-download.svg';
+import InfoIcon from '../../icons/ic-info.svg';
import { toDirective } from './utils';
import { FunctionalComponent } from 'preact';
@@ -56,6 +57,7 @@ const ICONS = {
user: UserIcon,
copy: CopyIcon,
download: DownloadIcon,
+ info: InfoIcon,
};
export type IconType = keyof typeof ICONS;
diff --git a/app/assets/javascripts/components/IconButton.tsx b/app/assets/javascripts/components/IconButton.tsx
index 8c0920c64..e15517419 100644
--- a/app/assets/javascripts/components/IconButton.tsx
+++ b/app/assets/javascripts/components/IconButton.tsx
@@ -27,7 +27,7 @@ export const IconButton: FunctionComponent
= ({
};
return (