Skip to content
52 changes: 27 additions & 25 deletions apps/class-solid/src/components/Analysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,31 +80,33 @@ export function TimeSeriesPlot() {
/** Simply show the final height for each experiment that has output */
function FinalHeights() {
return (
<For each={experiments}>
{(experiment, i) => {
const h =
experiment.reference.output?.h[
experiment.reference.output.h.length - 1
] || 0;
return (
<div class="mb-2">
<p>
{experiment.name}: {h.toFixed()} m
</p>
<For each={experiment.permutations}>
{(perm) => {
const h = perm.output?.h[perm.output.h.length - 1] || 0;
return (
<p>
{experiment.name}/{perm.name}: {h.toFixed()} m
</p>
);
}}
</For>
</div>
);
}}
</For>
<ul>
<For each={experiments}>
{(experiment) => {
const h = () =>
experiment.reference.output?.h[
experiment.reference.output.h.length - 1
] || 0;
return (
<>
<li class="mb-2" title={experiment.name}>
{experiment.name}: {h().toFixed()} m
</li>
<For each={experiment.permutations}>
{(perm) => {
const h = () => perm.output?.h[perm.output.h.length - 1] || 0;
return (
<li title={`${experiment.name}/${perm.name}`}>
{experiment.name}/{perm.name}: {h().toFixed()} m
</li>
);
}}
</For>
</>
);
}}
</For>
</ul>
);
}

Expand Down
7 changes: 4 additions & 3 deletions apps/class-solid/src/components/Experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
createUniqueId,
onCleanup,
} from "solid-js";

import { Button, buttonVariants } from "~/components/ui/button";
import { createArchive, toConfigBlob } from "~/lib/download";
import {
Expand All @@ -18,6 +17,7 @@ import {
} from "~/lib/store";
import { ExperimentConfigForm } from "./ExperimentConfigForm";
import { PermutationsList } from "./PermutationsList";
import { ShareButton } from "./ShareButton";
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
import {
Card,
Expand Down Expand Up @@ -188,8 +188,8 @@ function DownloadExperimentArchive(props: { experiment: Experiment }) {

const filename = `class-${props.experiment.name}.zip`;
return (
<a href={url()} download={filename} type="application/json">
Config + output
<a href={url()} download={filename} type="application/zip">
Configuration and output
</a>
);
}
Expand Down Expand Up @@ -271,6 +271,7 @@ export function ExperimentCard(props: {
>
<MdiDelete />
</Button>
<ShareButton experiment={experiment} />
</Show>
</CardFooter>
</Card>
Expand Down
6 changes: 3 additions & 3 deletions apps/class-solid/src/components/PermutationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function AddPermutationButton(props: {
experimentIndex: number;
}) {
const [open, setOpen] = createSignal(false);
const permutationName = `${props.experiment.permutations.length + 1}`;
const permutationName = () => `${props.experiment.permutations.length + 1}`;
return (
<Dialog open={open()} onOpenChange={setOpen}>
<DialogTrigger
Expand All @@ -118,14 +118,14 @@ function AddPermutationButton(props: {
id="add-permutation-form"
reference={props.experiment.reference.config}
config={{}}
permutationName={permutationName}
permutationName={permutationName()}
onSubmit={(config) => {
const { title, description, ...strippedConfig } = config;
setPermutationConfigInExperiment(
props.experimentIndex,
-1,
strippedConfig,
title ?? permutationName,
title ?? permutationName(),
);
setOpen(false);
}}
Expand Down
110 changes: 110 additions & 0 deletions apps/class-solid/src/components/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { type Accessor, Show, createMemo, createSignal } from "solid-js";
import { Button } from "~/components/ui/button";
import { encodeExperiment } from "~/lib/encode";
import type { Experiment } from "~/lib/store";
import {
MdiClipboard,
MdiClipboardCheck,
MdiShareVariantOutline,
} from "./icons";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { TextField, TextFieldInput } from "./ui/text-field";
import { showToast } from "./ui/toast";

export function ShareButton(props: { experiment: Accessor<Experiment> }) {
const [open, setOpen] = createSignal(false);
const [isCopied, setIsCopied] = createSignal(false);
let inputRef: HTMLInputElement | undefined;
const shareableLink = createMemo(() => {
if (!open()) {
return "";
}
const encodedExperiment = encodeExperiment(props.experiment());
const url = `${window.location.origin}#${encodedExperiment}`;
return url;
});

async function copyToClipboard() {
try {
await navigator.clipboard.writeText(shareableLink());
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); // Reset copied state after 2 seconds
showToast({
title: "Share link copied to clipboard",
duration: 1000,
});
} catch (err) {
console.error("Failed to copy text: ", err);
}
}

const handleOpenChange = (open: boolean) => {
setOpen(open);
if (open) {
setTimeout(() => {
inputRef?.focus();
inputRef?.select();
}, 0);
}
};

return (
<Dialog open={open()} onOpenChange={handleOpenChange}>
<DialogTrigger variant="outline" as={Button<"button">}>
<MdiShareVariantOutline />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle class="mr-10">Share link</DialogTitle>
<DialogDescription>
Anyone with{" "}
<a
target="_blank"
rel="noreferrer"
class="font-medium underline underline-offset-4"
href={shareableLink()}
>
this link
</a>{" "}
will be able to view the current experiment in their web browser.
</DialogDescription>
</DialogHeader>

<div class="flex items-center space-x-2">
<TextField class="w-full" defaultValue={shareableLink()}>
<TextFieldInput
ref={inputRef}
type="text"
readonly
class="w-full"
aria-label="Shareable link for current experiment"
/>
</TextField>
<Button
type="submit"
variant="outline"
size="icon"
class="px-3"
onClick={copyToClipboard}
aria-label={isCopied() ? "Link copied" : "Copy link"}
>
<span class="sr-only">Copy</span>
<Show when={isCopied()} fallback={<MdiClipboard />}>
<MdiClipboardCheck />
</Show>
</Button>
</div>
<div aria-live="polite" class="sr-only">
<Show when={isCopied()}>Link copied to clipboard</Show>
</div>
</DialogContent>
</Dialog>
);
}
26 changes: 22 additions & 4 deletions apps/class-solid/src/components/UploadExperiment.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { uploadExperiment } from "~/lib/store";
import { showToast } from "./ui/toast";

export function UploadExperiment() {
let ref: HTMLInputElement | undefined;
Expand All @@ -17,10 +18,27 @@ export function UploadExperiment() {
return;
}
const file = event.target.files[0];
file.text().then((body) => {
const rawData = JSON.parse(body);
uploadExperiment(rawData);
});
file
.text()
.then((body) => {
const rawData = JSON.parse(body);
return uploadExperiment(rawData);
})
.then(() => {
showToast({
title: "Experiment uploaded",
variant: "success",
duration: 1000,
});
})
.catch((error) => {
console.error(error);
showToast({
title: "Failed to upload experiment",
description: `${error}`,
variant: "error",
});
});
}
return (
<>
Expand Down
54 changes: 54 additions & 0 deletions apps/class-solid/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,57 @@ export function MdiExclamationThick(props: JSX.IntrinsicElements["svg"]) {
</svg>
);
}

export function MdiShareVariantOutline(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81c1.66 0 3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91s-1.31-2.92-2.92-2.92M18 4c.55 0 1 .45 1 1s-.45 1-1 1s-1-.45-1-1s.45-1 1-1M6 13c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1m12 7c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1"
/>
<title>Share experiment</title>
</svg>
);
}

export function MdiClipboard(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M19 3h-4.18C14.4 1.84 13.3 1 12 1s-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-7 0a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1"
/>
<title>Copy to clipboard</title>
</svg>
);
}

export function MdiClipboardCheck(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="m10 17l-4-4l1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1a1 1 0 0 1-1 1a1 1 0 0 1-1-1a1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1s-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2"
/>
<title>Copied to clipboard</title>
</svg>
);
}
Loading