Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share link #55

Merged
merged 11 commits into from
Oct 17, 2024
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