feat: add transform system
All checks were successful
/ deploy_site (push) Successful in 3m56s

This commit is contained in:
Minhyeok Park 2024-10-27 12:34:54 +09:00
parent beba067305
commit 87a35aeffc
Signed by: pmh_only
SSH Key Fingerprint: SHA256:g/OyGvi2pcd8ub9mqge/ohmDP0fZX/xOPWPIcM+9XpI
11 changed files with 191 additions and 26 deletions

View File

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.1.1", "@fontsource-variable/jetbrains-mono": "^5.1.1",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"clsx": "^2.1.1",
"framer-motion": "^11.11.10", "framer-motion": "^11.11.10",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -14,6 +14,9 @@ importers:
'@monaco-editor/react': '@monaco-editor/react':
specifier: ^4.6.0 specifier: ^4.6.0
version: 4.6.0(monaco-editor@0.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 4.6.0(monaco-editor@0.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
clsx:
specifier: ^2.1.1
version: 2.1.1
framer-motion: framer-motion:
specifier: ^11.11.10 specifier: ^11.11.10
version: 11.11.10(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 11.11.10(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -702,6 +705,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@ -1859,6 +1866,8 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
clsx@2.1.1: {}
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4

View File

@ -8,11 +8,14 @@ export const Button = styled.button`
background-color: #ff1696; background-color: #ff1696;
color: white; color: white;
transition: background-color 0.25s ease-out;
&:hover { &:hover {
background-color: #da1c9b; background-color: #da1c9b;
} }
&:placeholder { &:disabled {
color: gray; background-color: gray;
cursor: inherit;
} }
` `

View File

@ -3,11 +3,11 @@ import { motion } from "framer-motion";
import style from './style.module.scss' import style from './style.module.scss'
import { TransformGridItem } from "../TransformGridItem"; import { TransformGridItem } from "../TransformGridItem";
import { transforms } from "../Transforms/Transform"; import { transforms, wrapTransform } from "../Transforms/Transform";
export const TransformGrid: FC = () => export const TransformGrid: FC = () =>
<ul className={style.grid}> <ul className={style.grid}>
{transforms.map((v, i) => {transforms.map((transform, i) =>
<motion.li <motion.li
key={i} key={i}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@ -15,7 +15,7 @@ export const TransformGrid: FC = () =>
transition={{ delay: i * 0.1 + 1.5 }} > transition={{ delay: i * 0.1 + 1.5 }} >
<TransformGridItem <TransformGridItem
transform={v}/> transform={wrapTransform(transform)}/>
</motion.li> </motion.li>
)} )}

View File

@ -1,7 +1,9 @@
.grid { .grid {
padding: 10px; padding: 10px;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
align-content: flex-start;
gap: 10px; gap: 10px;
height: 100%; height: 100%;

View File

@ -1,30 +1,94 @@
import { FC } from "react"; import { ChangeEvent, FC, useState } from "react";
import style from './style.module.scss' import style from './style.module.scss'
import { Button } from "../Components/Button"; import { Button } from "../Components/Button";
import { Input } from "../Components/Input"; import { Input } from "../Components/Input";
import { TextArea } from "../Components/TextArea"; import { TextArea } from "../Components/TextArea";
import { Transform } from "../Transforms/Transform"; import { TransformCheckboxOption, TransformOption, TransformTextboxOption, WrappedTransform } from "../Transforms/Transform";
import { useRecoilState } from "recoil";
import { EditorValueState } from "../GlobalStates/EditorValueState";
import clsx from "clsx";
interface TransformGridItemProp { interface TransformGridItemProp {
transform: Transform transform: WrappedTransform
} }
export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => { export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => {
const [value, setValue] = useRecoilState(EditorValueState)
const [options, setOptions] = useState(new Map<string, TransformOption>())
const result = transform.fn(value, options)
const onCheckboxOptionChanged =
(option: TransformCheckboxOption) =>
(event: ChangeEvent<HTMLInputElement>) => {
options.set(option.key, {
...option,
value: event.target.checked
})
setOptions(new Map(options))
}
const onTextboxOptionChanged =
(option: TransformTextboxOption) =>
(event: ChangeEvent<HTMLInputElement>) => {
options.set(option.key, {
...option,
value: event.target.value
})
setOptions(new Map(options))
}
const onForwardButtonPressed = () => {
if (result.error)
return
setValue(result.value)
}
return ( return (
<div className={style.item}> <div className={style.item}>
<div className={style.toolbar}> <div className={style.toolbar}>
<Button>&lt;&lt;&lt;</Button> <Button disabled={result.error} onClick={onForwardButtonPressed}>
&lt;&lt;&lt;
</Button>
<h2 className={style.name}>{transform.name}</h2> <h2 className={style.name}>{transform.name}</h2>
<div className={style.options}> <div className={style.options}>
<label className={style.optionItem}>hello: <Input type="checkbox" /></label> {transform.options
<label className={style.optionItem}>hello: <Input type="text" /></label> ?.filter((option) => option.type === 'CHECKBOX')
<label className={style.optionItem}>hello: <Input type="checkbox" /></label> .map((option, i) => (
<label className={style.optionItem}>hello: <Input type="checkbox" /></label> <label key={i} className={style.optionItem}>
<p>{option.label ?? option.key}:</p>
<Input
onChange={onCheckboxOptionChanged(option)}
type="checkbox" />
</label>))}
{transform.options
?.filter((option) => option.type === 'TEXTBOX')
.map((option, i) => (
<label key={i} className={style.optionItem}>
<p>{option.label ?? option.key}:</p>
<Input
onChange={onTextboxOptionChanged(option)}
type="checkbox" />
</label>))}
</div> </div>
</div> </div>
<TextArea placeholder="(empty)" className={style.input} /> <TextArea
value={result.value}
readOnly
placeholder="(empty)"
className={clsx(result.error && style.error)} />
</div> </div>
) )
} }

View File

@ -4,7 +4,7 @@
display: flex; display: flex;
padding: 10px; padding: 10px;
flex-direction: column; flex-direction: column;
min-height: 300px; min-height: 250px;
.toolbar { .toolbar {
display: flex; display: flex;
@ -16,7 +16,7 @@
.name { .name {
font-size: inherit; font-size: inherit;
padding: 14px 0px; padding: 14px 6px;
} }
.options { .options {
@ -32,7 +32,12 @@
.optionItem { .optionItem {
display: flex; display: flex;
gap: 6px;
} }
} }
} }
.error {
color: #ffcaca;
}
} }

View File

@ -1,6 +0,0 @@
import { Transform } from "./Transform";
export const Base64DecodeTransform: Transform = {
name: 'base64d',
fn: () => 'Hello, world!'
}

View File

@ -0,0 +1,11 @@
import { Transform } from "./Transform";
export const Base64DecodeTransform: Transform = {
name: 'base64d',
fn: (v) => btoa(v)
}
export const Base64EncodeTransform: Transform = {
name: 'base64e',
fn: (v) => atob(v)
}

View File

@ -1,12 +1,59 @@
import { Base64DecodeTransform } from "./Base64DecodeTransform" import { Base64DecodeTransform, Base64EncodeTransform } from "./Base64Transforms"
import { URIDecodeTransform, URIEncodeTransform } from "./URITransforms"
export interface TransformCheckboxOption {
type: 'CHECKBOX',
key: string,
label?: string,
value?: boolean
}
export interface TransformTextboxOption {
type: 'TEXTBOX',
key: string,
label?: string,
value?: string
}
export type TransformOption =
TransformCheckboxOption | TransformTextboxOption
export interface Transform { export interface Transform {
name: string name: string
fn: () => string fn: (value: string, options: Map<string, TransformOption>) => string
options?: TransformOption[]
} }
export interface WrappedTransform extends Omit<Transform, 'fn'> {
wrapped: true,
fn: (value: string, options: Map<string, TransformOption>) => {
error: boolean,
value: string
}
}
export const wrapTransform = (transform: Transform): WrappedTransform => ({
...transform,
fn: (...args) => {
try {
return {
error: false,
value: transform.fn(...args)
}
} catch (err: any) {
return {
error: true,
value: err.toString()
}
}
},
wrapped: true
})
export const transforms: Transform[] = [ export const transforms: Transform[] = [
Base64DecodeTransform, Base64DecodeTransform,
Base64DecodeTransform, Base64EncodeTransform,
Base64DecodeTransform, URIDecodeTransform,
URIEncodeTransform
] ]

View File

@ -0,0 +1,29 @@
import { Transform } from "./Transform";
export const URIDecodeTransform: Transform = {
name: 'urid',
fn: (v, o) =>
o.get('cmp')?.value === true
? decodeURIComponent(v)
: decodeURI(v),
options: [{
type: 'CHECKBOX',
key: 'cmp'
}]
}
export const URIEncodeTransform: Transform = {
name: 'urie',
fn: (v, o) =>
o.get('cmp')?.value === true
? encodeURIComponent(v)
: encodeURI(v),
options: [{
type: 'CHECKBOX',
key: 'cmp'
}]
}