feat: recover embed code

This commit is contained in:
TrèsAbhi committed 2024-10-26 14:33:04 -05:00
1 parent cee082ab67
commit 9daab37ba1
16 files changed
+746

No files matched your search

@@ -0,0 +1,19 @@
import { Switch } from '@radix-ui/themes';
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState } from '../../stores/embedState';
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return (
<Switch
variant="soft"
checked={EmbedState.use((state) => state[configKey] as boolean)}
onCheckedChange={() => {
mutateEmbedState((draft) => {
draft[configKey] = !draft[configKey];
});
}}
/>
);
}
@@ -0,0 +1,19 @@
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState } from '../../stores/embedState';
import { ColorControllerRaw } from './ColorControllerRaw';
export function Color({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
const color = EmbedState.use((state) => state[configKey] as string);
return (
<ColorControllerRaw
value={color}
onValueChange={(value) => {
mutateEmbedState((draft) => {
draft[configKey] = value;
});
}}
/>
);
}
@@ -0,0 +1,38 @@
import { Slider } from '@radix-ui/themes';
interface ColorControllerRawProps {
value: string;
onValueChange: (value: string) => void;
}
export function ColorControllerRaw({
value,
onValueChange,
}: ColorControllerRawProps) {
const tintString = value.slice(0, 7);
const alphaString = value.slice(7, 9);
const alpha = alphaString.length === 0 ? 255 : parseInt(alphaString, 16);
return (
<>
<input
type="color"
value={value.slice(0, 7)}
onChange={(event) => {
let alpha = value.slice(7, 9);
if (alpha.length === 0) alpha = 'ff';
onValueChange(`${event.target.value}${alpha}`);
}}
/>
<Slider
variant="soft"
min={0}
max={255}
value={[alpha]}
onValueChange={([value]) => {
onValueChange(`${tintString}${value.toString(16)}`);
}}
/>
</>
);
}
@@ -0,0 +1,36 @@
import { Select } from '@radix-ui/themes';
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState } from '../../stores/embedState';
import type {
EmbedConfigItemType,
EmbedItemType,
} from '../../stores/embedState/constants';
export function Enum({
configKey,
config,
}: EmbedPreviewControllerProps & {
config: EmbedConfigItemType<EmbedItemType.Enum>;
}) {
const mutateEmbedState = EmbedState.useMutation();
return (
<Select.Root
value={EmbedState.use((state) => state[configKey] as string)}
onValueChange={(value) => {
mutateEmbedState((draft) => {
draft[configKey] = value;
});
}}
>
<Select.Trigger />
<Select.Content>
{config.options.map((option) => (
<Select.Item key={option.value} value={option.value}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
);
}
@@ -0,0 +1,80 @@
import {
AlertDialog,
Button,
Flex,
Heading,
Text,
TextField,
Tooltip,
} from '@radix-ui/themes';
import { useRef, useState } from 'react';
import { EmbedState } from '../../stores/embedState';
export function Import() {
const importInput = useRef<HTMLInputElement>(null);
const [showInvalidURLPrompt, setShowInvalidURLPrompt] = useState(false);
const [showConfirmation, setConfirmation] = useState(false);
const mutateEmbedState = EmbedState.useMutation();
return (
<>
<Heading>Import</Heading>
<Text size="2" color="gray" mb="2">
Paste your current URL if you have one already to import your settings
</Text>
<AlertDialog.Root
open={showInvalidURLPrompt}
onOpenChange={setShowInvalidURLPrompt}
>
<AlertDialog.Content>
<AlertDialog.Title>Invalid URL</AlertDialog.Title>
<AlertDialog.Description>
Try pasting the URL that BlitzKit previously generated, you silly
goose!
</AlertDialog.Description>
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Cancel>
<Button>Okay, my bad</Button>
</AlertDialog.Cancel>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<Flex gap="2" mb="6">
<TextField.Root
ref={importInput}
placeholder="Paste URL..."
style={{ flex: 1 }}
/>
<Tooltip content="Applied!" open={showConfirmation}>
<Button
onClick={() => {
if (!importInput.current) return;
try {
const url = new URL(importInput.current.value);
const state = url.searchParams.get('state');
if (state === null) throw undefined;
const json = JSON.parse(state);
mutateEmbedState((draft) => {
Object.assign(draft, json);
});
importInput.current.value = '';
setConfirmation(true);
setTimeout(() => setConfirmation(false), 2000);
} catch {
setShowInvalidURLPrompt(true);
}
}}
>
Apply
</Button>
</Tooltip>
</Flex>
</>
);
}
@@ -0,0 +1,26 @@
import { keyframes, style } from '@vanilla-extract/css';
const fadeOutBorder = keyframes({
from: {
outline: '1rem solid #ff0000ff',
},
to: {
outline: '1rem solid #ff000000',
},
});
export const wrapper = style({
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
overflow: 'hidden',
});
export const animated = style({
animation: `${fadeOutBorder} 0.5s linear`,
':hover': {
outline: '1rem solid #ff000080',
},
});
@@ -0,0 +1,36 @@
import { Box } from '@radix-ui/themes';
import { useEffect, useRef } from 'react';
import { previews } from '../../../constants/embeds';
import { useEmbedStateCurry } from '../../../stores/embedState/utilities';
import * as styles from './index.css';
interface PreviewWrapperProps {
name: keyof typeof previews;
}
export function PreviewWrapper({ name }: PreviewWrapperProps) {
const { useState } = useEmbedStateCurry();
const width = useState('width');
const height = useState('height');
const Preview = previews[name];
const wrapper = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!wrapper.current) return;
wrapper.current.classList.remove(styles.animated);
void wrapper.current.offsetWidth;
wrapper.current.classList.add(styles.animated);
}, [width, height]);
return (
<Box
ref={wrapper}
className={styles.wrapper}
width={`${width}px`}
height={`${height}px`}
>
<Preview />
</Box>
);
}
@@ -0,0 +1,26 @@
import { Slider } from '@radix-ui/themes';
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState, type RadixRadius } from '../../stores/embedState';
export function Radius({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return (
<Slider
variant="soft"
min={0}
max={5}
value={[
EmbedState.use((state) => {
const radius = state[configKey] as RadixRadius;
return radius === 'full' ? 5 : parseInt(radius);
}),
]}
onValueChange={([value]) => {
mutateEmbedState((draft) => {
draft[configKey] = value === 5 ? 'full' : `${value}`;
});
}}
/>
);
}
@@ -0,0 +1,32 @@
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState } from '../../stores/embedState';
import type {
EmbedConfigItemType,
EmbedItemType,
} from '../../stores/embedState/constants';
import { ColorControllerRaw } from './ColorControllerRaw';
export function RichText({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
const state = EmbedState.use(
(state) =>
state[
configKey
] as EmbedConfigItemType<EmbedItemType.RichText>['default'],
);
return (
<ColorControllerRaw
value={state.color}
onValueChange={(value) => {
mutateEmbedState((draft) => {
(
draft[
configKey
] as EmbedConfigItemType<EmbedItemType.RichText>['default']
).color = value;
});
}}
/>
);
}
@@ -0,0 +1,23 @@
import { Slider } from '@radix-ui/themes';
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState, type RadixSize } from '../../stores/embedState';
export function Size({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return (
<Slider
variant="soft"
min={0}
max={9}
value={[
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
]}
onValueChange={([value]) => {
mutateEmbedState((draft) => {
draft[configKey] = `${value}`;
});
}}
/>
);
}
@@ -0,0 +1,25 @@
import { Slider } from '@radix-ui/themes';
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState, type RadixSizeWithout0 } from '../../stores/embedState';
export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return (
<Slider
variant="soft"
min={1}
max={9}
value={[
EmbedState.use((state) =>
parseInt(state[configKey] as RadixSizeWithout0),
),
]}
onValueChange={([value]) => {
mutateEmbedState((draft) => {
draft[configKey] = `${value}`;
});
}}
/>
);
}
@@ -0,0 +1,32 @@
import { Slider as RadixSlider } from '@radix-ui/themes';
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState, type RadixSize } from '../../stores/embedState';
import type {
EmbedConfigItemType,
EmbedItemType,
} from '../../stores/embedState/constants';
export function Slider({
configKey,
config,
}: EmbedPreviewControllerProps & {
config: EmbedConfigItemType<EmbedItemType.Slider>;
}) {
const mutateEmbedState = EmbedState.useMutation();
return (
<RadixSlider
variant="soft"
min={config.min}
max={config.max}
value={[
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
]}
onValueChange={([value]) => {
mutateEmbedState((draft) => {
draft[configKey] = value;
});
}}
/>
);
}
@@ -0,0 +1,19 @@
import { TextField } from '@radix-ui/themes';
import type { EmbedPreviewControllerProps } from '../../pages/tools/embed/[embed]/_index';
import { EmbedState } from '../../stores/embedState';
export function TextController({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return (
<TextField.Root
placeholder="Empty"
value={EmbedState.use((state) => state[configKey] as string)}
onChange={(event) => {
mutateEmbedState((draft) => {
draft[configKey] = event.target.value;
});
}}
/>
);
}
@@ -0,0 +1,303 @@
import { imgur } from '@blitzkit/core';
import {
CodeIcon,
HeightIcon,
ImageIcon,
Link2Icon,
ResetIcon,
TimerIcon,
WidthIcon,
} from '@radix-ui/react-icons';
import { Box, Button, Flex, Heading, ScrollArea, Text } from '@radix-ui/themes';
import { capitalize, startCase } from 'lodash-es';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { stringify } from 'urlon';
import { CopyButton } from '../../../../components/CopyButton';
import { Boolean } from '../../../../components/Embeds/Boolean';
import { Color } from '../../../../components/Embeds/Color';
import { Enum } from '../../../../components/Embeds/Enum';
import { Import } from '../../../../components/Embeds/Import';
import { PreviewWrapper } from '../../../../components/Embeds/PreviewWrapper';
import { Radius } from '../../../../components/Embeds/Radius';
import { RichText } from '../../../../components/Embeds/RichText';
import { Size } from '../../../../components/Embeds/Size';
import { SizeWithout0 } from '../../../../components/Embeds/SizeWithout0';
import { Slider } from '../../../../components/Embeds/Slider';
import { TextController } from '../../../../components/Embeds/TextController';
import { PageWrapper } from '../../../../components/PageWrapper';
import {
configurations,
extractEmbedConfigDefaults,
} from '../../../../constants/embeds';
import { NAVBAR_HEIGHT } from '../../../../constants/navbar';
import { App } from '../../../../stores/app';
import {
EmbedState,
type EmbedConfig,
type EmbedStateStore,
} from '../../../../stores/embedState';
import { EmbedItemType } from '../../../../stores/embedState/constants';
export interface EmbedPreviewControllerProps {
configKey: string;
}
interface PageProps {
embed: keyof typeof configurations;
}
export function Page({ embed }: PageProps) {
const config = configurations[embed];
return (
<App.Provider>
<EmbedState.Provider data={extractEmbedConfigDefaults(config)}>
<Content embed={embed} />
</EmbedState.Provider>
</App.Provider>
);
}
function Content({ embed }: PageProps) {
const embedStateStore = EmbedState.useStore();
const appStore = App.useStore();
const config = configurations[embed] as EmbedConfig;
const [backgroundImage, setBackgroundImage] = useState(imgur('SO13zur'));
const fileInput = useRef<HTMLInputElement>();
useEffect(() => {
fileInput.current = document.createElement('input');
fileInput.current.type = 'file';
fileInput.current.style.display = 'none';
fileInput.current.accept = 'image/*';
fileInput.current.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
setBackgroundImage(URL.createObjectURL(file));
};
}, []);
return (
<PageWrapper padding="0" size={1028 + 640} color="red">
<Flex>
<ScrollArea
scrollbars="vertical"
style={{ height: '100%', maxWidth: 320 }}
>
<Flex direction="column" gap="2" p="4">
<Import />
<Heading>Export</Heading>
<Text size="2" color="gray" mb="2">
Copy all properties for you streaming software
</Text>
<Flex mb="6" gap="2" wrap="wrap">
<CopyButton
copy={() => {
const { wargaming } = appStore.getState().logins;
if (!wargaming) return;
const state = embedStateStore.getState();
const initial = embedStateStore.getInitialState();
const shallowState: EmbedStateStore = {};
Object.entries(state).forEach(([key, value]) => {
if (value !== initial[key]) {
shallowState[key] = value;
}
});
const searchParams = new URLSearchParams({
id: `${wargaming.id}`,
state: stringify(shallowState),
});
return `${location.origin}/tools/embed/${embed}/host?${searchParams.toString()}`;
}}
>
<Link2Icon />
Copy URL
</CopyButton>
<CopyButton
variant="outline"
copy={() =>
'body { margin: 0; background-color: transparent; overflow: hidden; }'
}
>
<CodeIcon />
Copy CSS
</CopyButton>
<CopyButton
variant="outline"
copy={() => `${embedStateStore.getState().width}`}
>
<WidthIcon />
Copy width
</CopyButton>
<CopyButton
variant="outline"
copy={() => `${embedStateStore.getState().height}`}
>
<HeightIcon />
Copy height
</CopyButton>
<CopyButton variant="outline" copy={() => '`'}>
<TimerIcon />
Copy framerate
</CopyButton>
</Flex>
<Flex justify="between" align="center">
<Heading>Customize</Heading>
<Button
color="red"
variant="ghost"
onClick={() => {
embedStateStore.setState(
embedStateStore.getInitialState(),
true,
);
}}
>
<ResetIcon /> Reset
</Button>
</Flex>
<Text size="2" color="gray" mb="2">
Bells and whistles of your embed
</Text>
{Object.keys(config).map((configKey) => {
const item = config[configKey];
let control: ReactNode;
let oneLiner = false;
switch (item.type) {
case EmbedItemType.Boolean: {
control = <Boolean configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.Color: {
control = <Color configKey={configKey} />;
break;
}
case EmbedItemType.Radius: {
control = <Radius configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.Size: {
control = <Size configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.SizeWithout0: {
control = <SizeWithout0 configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.Slider: {
control = <Slider configKey={configKey} config={item} />;
oneLiner = true;
break;
}
case EmbedItemType.String: {
control = <TextController configKey={configKey} />;
break;
}
case EmbedItemType.RichText: {
control = <RichText configKey={configKey} />;
break;
}
case EmbedItemType.Enum: {
control = <Enum configKey={configKey} config={item} />;
oneLiner = true;
break;
}
}
return (
<Flex
direction={oneLiner ? undefined : 'column'}
gap={oneLiner ? '2' : '1'}
justify={oneLiner ? 'between' : undefined}
mb={oneLiner ? '2' : '4'}
pb={item.pad ? '6' : undefined}
>
<Text>{capitalize(startCase(configKey as string))}</Text>
<Flex
gap="2"
align="center"
flexGrow="1"
justify={oneLiner ? 'end' : undefined}
>
{control}
</Flex>
</Flex>
);
})}
</Flex>
</ScrollArea>
<Box
p="4"
flexGrow="1"
position="sticky"
top={`${NAVBAR_HEIGHT}px`}
height={`calc(100vh - ${NAVBAR_HEIGHT}px)`}
>
<Flex
direction="column"
height="100%"
style={{
borderRadius: 'var(--radius-3)',
overflow: 'hidden',
boxShadow: 'var(--shadow-3)',
}}
>
<Flex
justify="between"
align="center"
p="2"
pl="3"
style={{
backgroundColor: 'var(--color-panel-solid)',
}}
>
<Text color="gray">Example preview</Text>
<Button
variant="outline"
color="gray"
onClick={() => fileInput.current?.click()}
>
<ImageIcon /> Upload test background
</Button>
</Flex>
<Box
flexGrow="1"
style={{
background: `url(${backgroundImage}) center / cover no-repeat`,
}}
position="relative"
overflow="hidden"
>
<PreviewWrapper name={embed} />
</Box>
</Flex>
</Box>
</Flex>
</PageWrapper>
);
}
@@ -0,0 +1,16 @@
---
import type { GetStaticPaths } from 'astro';
import { configurations } from '../../../../constants/embeds';
import BaseLayout from '../../../../layouts/BaseLayout.astro';
import { Page } from './_index';
export const getStaticPaths = (() => {
return Object.keys(configurations).map((embed) => ({
params: { embed: embed as keyof typeof configurations },
}));
}) satisfies GetStaticPaths;
---
<BaseLayout>
<Page client:load embed={Astro.params.embed} />
</BaseLayout>
@@ -0,0 +1,16 @@
---
import { PageWrapper } from '../../../components/PageWrapper';
import { configurations } from '../../../constants/embeds';
import { embedTool } from '../../../constants/tools';
import ToolLayout from '../../../layouts/ToolLayout.astro';
---
<ToolLayout tool={embedTool}>
<PageWrapper>
{
Object.entries(configurations).map(([embed, config]) => (
<a href={`/tools/embed/${embed}`}>{embed}</a>
))
}
</PageWrapper>
</ToolLayout>