Skip to content

Commit

Permalink
fully working idea
Browse files Browse the repository at this point in the history
  • Loading branch information
kermage committed Mar 23, 2024
1 parent 0ec4ce7 commit b41dcd0
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 1 deletion.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"preview": "vite preview"
},
"dependencies": {
"preact": "^10.19.6"
"@preact/signals": "^1.2.3",
"preact": "^10.19.6",
"shiki": "^1.2.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.8.2",
Expand Down
29 changes: 29 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
import { signal, useSignal } from '@preact/signals';
import CodePreview from './components/CodePreview';
import FieldList from './components/FieldList';

import { FIELD_TYPES, type FieldItemType } from './types';

const random = Math.floor(Math.random() * FIELD_TYPES.length);
const fields: FieldItemType[] = [
{
key: 'index',
type: FIELD_TYPES[random],
title: 'Example Title',
description: 'Test Description',
},
];

export default function () {
const fieldItems = useSignal(fields.map((field) => signal(field)));

return (
<div className="grid place-items-center gap-8 md:gap-16 lg:w-full">
<h1 className="text-center text-5xl font-black tracking-wide md:text-6xl">ThemePlate Fields</h1>

<div className="grid w-full bg-neutral-900 lg:grid-cols-2">
<FieldList items={fieldItems} initLength={fields.length}></FieldList>
<CodePreview data={fieldItems}></CodePreview>
</div>

<div class="flex items-center gap-4">
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" alt="Vite logo" />
Expand Down
11 changes: 11 additions & 0 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { HTMLAttributes } from 'preact/compat';

type Props = HTMLAttributes<HTMLButtonElement>;

export default function ({ children, ...props }: Props) {
return (
<button {...props} className="rounded border border-neutral-700 bg-neutral-800 px-6 py-2 text-sm text-neutral-200 hover:ring">
{children}
</button>
);
}
43 changes: 43 additions & 0 deletions src/components/CodePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { effect } from '@preact/signals';
import { useState } from 'preact/hooks';
import { codeToHtml } from 'shiki';

import type { Signal } from '@preact/signals';
import type { FieldItemType } from '../types';

interface Props {
data: Signal<Signal<FieldItemType>[]>;
}

const item = (value: FieldItemType) => {
const items = Object.entries(value)
.filter(([key]) => key !== 'key')
.map(([key, val]) => {
return `'${key}' => '${val}'`;
});

return `'${value.key}' => array(
${items.join(',\n\t\t')},
)`;
};

const wrap = (value: Signal<FieldItemType>[]) => {
const items = Object.values(value).map((val) => `${item(val.value)}`);

return `array(
${items.join(',\n\t')},
);`;
};

export default function ({ data }: Props) {
const [html, setHtml] = useState('');

effect(() => {
codeToHtml(wrap(data.value), {
lang: 'php',
theme: 'dark-plus',
}).then(setHtml);
});

return <div className="" dangerouslySetInnerHTML={{ __html: html }}></div>;
}
72 changes: 72 additions & 0 deletions src/components/FieldForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { FIELD_TYPES, type FieldItemType } from '../types';

type Props = {
values: FieldItemType;
save: (values: FieldItemType) => void;
};

const classes = {
label: 'grid grid-cols-4 gap-4 items-baseline',
field: 'appearance-none col-span-3 rounded bg-neutral-600 px-3 py-1',
};
export default function ({ values, save }: Props) {
const { title, key, description, type } = values;

const handleChange = (event: any) => {
const { name, value } = event.currentTarget;

save({
...values,
[name]: value,
});
};

return (
<>
<label className={classes.label}>
Key
<input
className={classes.field}
type="text"
name="key"
onInput={handleChange}
value={key}
/>
</label>

<label className={classes.label}>
Type
<select
className={classes.field}
name="type"
onChange={handleChange}
value={type}
>
{FIELD_TYPES.map((fType, index) => (
<option key={index} value={fType}>
{fType}
</option>
))}
</select>
</label>

<label className={classes.label}>
Title
<input
className={classes.field}
type="text"
name="title"
onInput={handleChange}
value={title}
/>
</label>

<label className={classes.label}>
Description
<textarea className={classes.field} name="description" onInput={handleChange}>
{description}
</textarea>
</label>
</>
);
}
41 changes: 41 additions & 0 deletions src/components/FieldItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useComputed, useSignal } from '@preact/signals';
import FieldForm from './FieldForm';

import type { Signal } from '@preact/signals';
import type { FieldItemType } from '../types';

type Props = {
defaultOpen: boolean;
id: number;
data: Signal<FieldItemType>;
};

export default function ({ defaultOpen, id, data }: Props) {
const title = useComputed(() => data.value.title);
const type = useComputed(() => data.value.type.toUpperCase());
const collapsed = useSignal(!defaultOpen);
const isCollapsed = useComputed(() => (collapsed.value ? 'true' : 'false'));
const className = useComputed(
() => `grid gap-2 border-neutral-600 p-4 ${collapsed.value ? 'hidden' : ''}`,
);

return (
<li key={id} className="rounded bg-neutral-800 text-white shadow">
<h2 id={`preview-${id}`} aria-hidden={isCollapsed} aria-controls={`form-${id}`}>
<button
className="p-4 grid w-full grid-cols-4 items-center gap-4 hover:ring"
onClick={() => {
collapsed.value = !collapsed.value;
}}
>
<span className="bg-neutral-600 p-1 text-xs">{type}</span>
<span className="col-span-3 text-left">{title}</span>
</button>

<form id={`form-${id}`} className={className} aria-labelledby={`preview-${id}`}>
<FieldForm values={data.peek()} save={(values) => (data.value = values)} />
</form>
</h2>
</li>
);
}
42 changes: 42 additions & 0 deletions src/components/FieldList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { signal, useComputed } from '@preact/signals';
import { FIELD_TYPES } from '../types';
import Button from './Button';
import FieldItem from './FieldItem';

import type { Signal } from '@preact/signals';
import type { FieldItemType } from '../types';

type Props = {
items: Signal<Signal<FieldItemType>[]>;
initLength: number;
};

export default function ({ items, initLength }: Props) {
const nextIndex = useComputed(() => items.value.length + 1);

const addField = () => {
items.value = [
...items.value,
signal({
key: `index-${nextIndex}`,
type: FIELD_TYPES[0],
title: `Title #${nextIndex}`,
description: `Description #${nextIndex}`,
}),
];
};

return (
<div className="grid auto-rows-min gap-4 p-4 lg:p-8">
<ol className="grid gap-4">
{items.value.map((field, index) => (
<FieldItem id={index} data={field} defaultOpen={index + 1 >= initLength} />
))}
</ol>

<div className="mt-2">
<Button onClick={() => addField()}>Add</Button>
</div>
</div>
);
}
20 changes: 20 additions & 0 deletions src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,23 @@
max-width: 1280px;
margin: 0 auto;
}

.shiki {
height: 100%;
padding: 2rem;
}

code {
position: sticky;
top: 2rem;
overflow: auto;
display: block;
}

select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23f0f0f0' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.25rem center;
background-repeat: no-repeat;
background-size: 1.5rem;
padding-right: 2rem;
}
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const FIELD_TYPES = [
'input',
'textarea',
'date',
'select',
'radio',
'checkbox',
'color',
'file',
'number',
'editor',
'html',
'link',
] as const;

export type FieldItemType = {
key: string;
type: (typeof FIELD_TYPES)[number];
title: string;
description: string;
};

0 comments on commit b41dcd0

Please sign in to comment.