From 4e4aaa19a6416a472b4a600135a3c367409b6904 Mon Sep 17 00:00:00 2001 From: stabldev Date: Wed, 18 Jun 2025 11:53:48 +0530 Subject: [PATCH 01/14] feat: add sidebar with sortable dnd context --- package.json | 5 ++ pnpm-lock.yaml | 82 +++++++++++++++++++ .../[repo]/new/_components/component-item.tsx | 34 ++++++++ .../(app)/~/[repo]/new/_components/editor.tsx | 12 +++ .../~/[repo]/new/_components/sidebar.tsx | 28 +++++++ .../~/[repo]/new/_constants/components.ts | 21 +++++ src/app/(app)/~/[repo]/new/page.tsx | 18 ++++ 7 files changed, 200 insertions(+) create mode 100644 src/app/(app)/~/[repo]/new/_components/component-item.tsx create mode 100644 src/app/(app)/~/[repo]/new/_components/editor.tsx create mode 100644 src/app/(app)/~/[repo]/new/_components/sidebar.tsx create mode 100644 src/app/(app)/~/[repo]/new/_constants/components.ts create mode 100644 src/app/(app)/~/[repo]/new/page.tsx diff --git a/package.json b/package.json index addc395..cdc89a4 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "pre-commit": "lint-staged" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dropdown-menu": "^2.1.15", @@ -25,6 +29,7 @@ "clsx": "^2.1.1", "dayjs": "^1.11.13", "lucide-react": "^0.513.0", + "nanoid": "^5.1.5", "next": "15.3.3", "next-auth": "5.0.0-beta.28", "next-themes": "^0.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2de3ff..12fa18a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,18 @@ importers: .: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.1.0) '@radix-ui/react-alert-dialog': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -38,6 +50,9 @@ importers: lucide-react: specifier: ^0.513.0 version: 0.513.0(react@19.1.0) + nanoid: + specifier: ^5.1.5 + version: 5.1.5 next: specifier: 15.3.3 version: 15.3.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -133,6 +148,34 @@ packages: nodemailer: optional: true + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -1973,6 +2016,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + napi-postinstall@0.2.4: resolution: {integrity: sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2635,6 +2683,38 @@ snapshots: preact: 10.24.3 preact-render-to-string: 6.5.11(preact@10.24.3) + '@dnd-kit/accessibility@3.1.1(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -4546,6 +4626,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.5: {} + napi-postinstall@0.2.4: {} natural-compare@1.4.0: {} diff --git a/src/app/(app)/~/[repo]/new/_components/component-item.tsx b/src/app/(app)/~/[repo]/new/_components/component-item.tsx new file mode 100644 index 0000000..b9a790a --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_components/component-item.tsx @@ -0,0 +1,34 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { GripVertical } from 'lucide-react'; + +interface Props { + id: string; + label: string; + Icon: React.ComponentType<{ className: string }>; +} + +export default function ComponentItem({ id, label, Icon }: Props) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + }); + + const style = { + transform: CSS.Translate.toString(transform), + transition, + zIndex: isDragging ? 1 : 0, + }; + + return ( +
+ + {label} + +
+ ); +} diff --git a/src/app/(app)/~/[repo]/new/_components/editor.tsx b/src/app/(app)/~/[repo]/new/_components/editor.tsx new file mode 100644 index 0000000..4a693f8 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_components/editor.tsx @@ -0,0 +1,12 @@ +export default function Editor() { + return ( +
+
+

Editor

+ + Arrange and edit frontmatter fields below + +
+
+ ); +} diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx new file mode 100644 index 0000000..6f9e152 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -0,0 +1,28 @@ +import { DndContext } from '@dnd-kit/core'; +import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { useState } from 'react'; +import { type ComponentsId, components } from '../_constants/components'; +import ComponentItem from './component-item'; + +export default function Sidebar() { + const [activeFieldIds] = useState>(['text', 'slug']); + + return ( + + +
+
+

Fields

+ Click on a field below to edit +
+
+ {Object.entries(components).map(([id, { label, Icon }]) => ( + + ))} +
+
+
+
+ ); +} diff --git a/src/app/(app)/~/[repo]/new/_constants/components.ts b/src/app/(app)/~/[repo]/new/_constants/components.ts new file mode 100644 index 0000000..c90f19e --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_constants/components.ts @@ -0,0 +1,21 @@ +import { TextCursorInput, Type } from 'lucide-react'; + +export type ComponentsId = 'text' | 'slug'; +export type Components = Record< + ComponentsId, + { + label: string; + Icon: React.ComponentType<{ className: string }>; + } +>; + +export const components: Components = { + text: { + label: 'Text Input', + Icon: Type, + }, + slug: { + label: 'Slug Input', + Icon: TextCursorInput, + }, +} as const; diff --git a/src/app/(app)/~/[repo]/new/page.tsx b/src/app/(app)/~/[repo]/new/page.tsx new file mode 100644 index 0000000..6e2ae4c --- /dev/null +++ b/src/app/(app)/~/[repo]/new/page.tsx @@ -0,0 +1,18 @@ +'use client'; + +import Editor from './_components/editor'; +import Sidebar from './_components/sidebar'; + +export default function Page() { + return ( + <> + {/* adding title like this cause of client component */} + New + {/* rest of the layout */} +
+ + +
+ + ); +} From 35340995175d12eaf06f6d684e0e19b218014311 Mon Sep 17 00:00:00 2001 From: stabldev Date: Wed, 18 Jun 2025 13:35:48 +0530 Subject: [PATCH 02/14] feat: handle sort logic --- .../[repo]/new/_components/component-item.tsx | 2 +- .../~/[repo]/new/_components/sidebar.tsx | 30 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/app/(app)/~/[repo]/new/_components/component-item.tsx b/src/app/(app)/~/[repo]/new/_components/component-item.tsx index b9a790a..0837f59 100644 --- a/src/app/(app)/~/[repo]/new/_components/component-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/component-item.tsx @@ -13,7 +13,7 @@ export default function ComponentItem({ id, label, Icon }: Props) { id, }); - const style = { + const style: React.CSSProperties = { transform: CSS.Translate.toString(transform), transition, zIndex: isDragging ? 1 : 0, diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index 6f9e152..ad437b0 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -1,15 +1,30 @@ -import { DndContext } from '@dnd-kit/core'; +import { DndContext, DragEndEvent } from '@dnd-kit/core'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; -import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useState } from 'react'; import { type ComponentsId, components } from '../_constants/components'; import ComponentItem from './component-item'; export default function Sidebar() { - const [activeFieldIds] = useState>(['text', 'slug']); + const [activeFieldIds, setActiveFieldIds] = useState>(['text', 'slug']); + + function handleDragEnd(e: DragEndEvent) { + const { active, over } = e; + + if (over && active.id !== over.id) { + setActiveFieldIds((items) => { + const oldIndex = items.indexOf(active.id as ComponentsId); + const newIndex = items.indexOf(over.id as ComponentsId); + + const newArr = arrayMove(items, oldIndex, newIndex); + console.log(newArr); + return newArr; + }); + } + } return ( - +
@@ -17,9 +32,10 @@ export default function Sidebar() { Click on a field below to edit
- {Object.entries(components).map(([id, { label, Icon }]) => ( - - ))} + {activeFieldIds.map((id) => { + const { label, Icon } = components[id]; + return ; + })}
From 787be6453b5442437de0e069dbbfdf98d51321e9 Mon Sep 17 00:00:00 2001 From: stabldev Date: Wed, 18 Jun 2025 13:41:30 +0530 Subject: [PATCH 03/14] chore: sticky sidebar and dynamic cursor --- .../(app)/~/[repo]/new/_components/component-item.tsx | 9 ++++++++- src/app/(app)/~/[repo]/new/_components/sidebar.tsx | 2 +- src/app/(app)/~/[repo]/new/page.tsx | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/(app)/~/[repo]/new/_components/component-item.tsx b/src/app/(app)/~/[repo]/new/_components/component-item.tsx index 0837f59..182bcd9 100644 --- a/src/app/(app)/~/[repo]/new/_components/component-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/component-item.tsx @@ -1,3 +1,4 @@ +import { cn } from '@/lib/utils'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { GripVertical } from 'lucide-react'; @@ -28,7 +29,13 @@ export default function ComponentItem({ id, label, Icon }: Props) { > {label} - + ); } diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index ad437b0..9c4fa8d 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -26,7 +26,7 @@ export default function Sidebar() { return ( -
+

Fields

Click on a field below to edit diff --git a/src/app/(app)/~/[repo]/new/page.tsx b/src/app/(app)/~/[repo]/new/page.tsx index 6e2ae4c..37f6bb8 100644 --- a/src/app/(app)/~/[repo]/new/page.tsx +++ b/src/app/(app)/~/[repo]/new/page.tsx @@ -10,7 +10,9 @@ export default function Page() { New {/* rest of the layout */}
- +
+ +
From 142f98daa72c801e38be306f963da63709542cd1 Mon Sep 17 00:00:00 2001 From: stabldev Date: Wed, 18 Jun 2025 18:57:08 +0530 Subject: [PATCH 04/14] fix: duplicate active field --- ...mponent-item.tsx => active-field-item.tsx} | 4 +- .../~/[repo]/new/_components/field-item.tsx | 18 +++++++ .../~/[repo]/new/_components/sidebar.tsx | 52 +++++++++++++------ .../components.ts => _lib/constants.ts} | 10 +--- src/app/(app)/~/[repo]/new/_lib/types.ts | 13 +++++ src/app/(app)/~/[repo]/new/page.tsx | 9 +++- 6 files changed, 77 insertions(+), 29 deletions(-) rename src/app/(app)/~/[repo]/new/_components/{component-item.tsx => active-field-item.tsx} (89%) create mode 100644 src/app/(app)/~/[repo]/new/_components/field-item.tsx rename src/app/(app)/~/[repo]/new/{_constants/components.ts => _lib/constants.ts} (56%) create mode 100644 src/app/(app)/~/[repo]/new/_lib/types.ts diff --git a/src/app/(app)/~/[repo]/new/_components/component-item.tsx b/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx similarity index 89% rename from src/app/(app)/~/[repo]/new/_components/component-item.tsx rename to src/app/(app)/~/[repo]/new/_components/active-field-item.tsx index 182bcd9..b3befb5 100644 --- a/src/app/(app)/~/[repo]/new/_components/component-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx @@ -9,13 +9,13 @@ interface Props { Icon: React.ComponentType<{ className: string }>; } -export default function ComponentItem({ id, label, Icon }: Props) { +export default function ActiveFieldItem({ id, label, Icon }: Props) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, }); const style: React.CSSProperties = { - transform: CSS.Translate.toString(transform), + transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 1 : 0, }; diff --git a/src/app/(app)/~/[repo]/new/_components/field-item.tsx b/src/app/(app)/~/[repo]/new/_components/field-item.tsx new file mode 100644 index 0000000..d50d21f --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_components/field-item.tsx @@ -0,0 +1,18 @@ +interface Props { + id: string; + label: string; + Icon: React.ComponentType<{ className: string }>; + onClick: (id: string) => void; +} + +export default function FieldItem({ id, label, Icon, onClick }: Props) { + return ( + + ); +} diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index 9c4fa8d..6812024 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -1,20 +1,25 @@ import { DndContext, DragEndEvent } from '@dnd-kit/core'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { useState } from 'react'; -import { type ComponentsId, components } from '../_constants/components'; -import ComponentItem from './component-item'; +import { nanoid } from 'nanoid'; +import { components } from '../_lib/constants'; +import { ActiveField, ComponentsId } from '../_lib/types'; +import ActiveFieldItem from './active-field-item'; +import FieldItem from './field-item'; -export default function Sidebar() { - const [activeFieldIds, setActiveFieldIds] = useState>(['text', 'slug']); +interface Props { + activeFields: ActiveField[]; + setActiveFields: React.Dispatch>; +} +export default function Sidebar({ activeFields, setActiveFields }: Props) { function handleDragEnd(e: DragEndEvent) { const { active, over } = e; if (over && active.id !== over.id) { - setActiveFieldIds((items) => { - const oldIndex = items.indexOf(active.id as ComponentsId); - const newIndex = items.indexOf(over.id as ComponentsId); + setActiveFields((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); const newArr = arrayMove(items, oldIndex, newIndex); console.log(newArr); @@ -23,22 +28,35 @@ export default function Sidebar() { } } + function addField(componentId: string) { + setActiveFields((prev) => [...prev, { id: `${componentId}-${nanoid()}`, componentId }]); + } + return ( - - -
+
+ +

Fields

Click on a field below to edit
- {activeFieldIds.map((id) => { - const { label, Icon } = components[id]; - return ; + {activeFields.map(({ id, componentId }) => { + const component = Object.keys(components).find((c) => c === componentId); + if (!component) return null; + + const { label, Icon } = components[component as ComponentsId]; + return ; })}
-
- - + + + Click on a field below to add +
+ {Object.entries(components).map(([id, { label, Icon }]) => ( + + ))} +
+
); } diff --git a/src/app/(app)/~/[repo]/new/_constants/components.ts b/src/app/(app)/~/[repo]/new/_lib/constants.ts similarity index 56% rename from src/app/(app)/~/[repo]/new/_constants/components.ts rename to src/app/(app)/~/[repo]/new/_lib/constants.ts index c90f19e..c72e516 100644 --- a/src/app/(app)/~/[repo]/new/_constants/components.ts +++ b/src/app/(app)/~/[repo]/new/_lib/constants.ts @@ -1,13 +1,5 @@ import { TextCursorInput, Type } from 'lucide-react'; - -export type ComponentsId = 'text' | 'slug'; -export type Components = Record< - ComponentsId, - { - label: string; - Icon: React.ComponentType<{ className: string }>; - } ->; +import { Components } from './types'; export const components: Components = { text: { diff --git a/src/app/(app)/~/[repo]/new/_lib/types.ts b/src/app/(app)/~/[repo]/new/_lib/types.ts new file mode 100644 index 0000000..844627c --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_lib/types.ts @@ -0,0 +1,13 @@ +export interface ActiveField { + id: string; + componentId: string; +} + +export type ComponentsId = 'text' | 'slug'; +export type Components = Record< + ComponentsId, + { + label: string; + Icon: React.ComponentType<{ className: string }>; + } +>; diff --git a/src/app/(app)/~/[repo]/new/page.tsx b/src/app/(app)/~/[repo]/new/page.tsx index 37f6bb8..ddedb65 100644 --- a/src/app/(app)/~/[repo]/new/page.tsx +++ b/src/app/(app)/~/[repo]/new/page.tsx @@ -1,9 +1,16 @@ 'use client'; +import { nanoid } from 'nanoid'; +import { useState } from 'react'; import Editor from './_components/editor'; import Sidebar from './_components/sidebar'; +import { ActiveField } from './_lib/types'; export default function Page() { + const [activeFields, setActiveFields] = useState>([ + { id: `text-${nanoid()}`, componentId: 'text' }, + ]); + return ( <> {/* adding title like this cause of client component */} @@ -11,7 +18,7 @@ export default function Page() { {/* rest of the layout */}
- +
From 6e80e0d87ae181c271a4dd2086118ff430b4e856 Mon Sep 17 00:00:00 2001 From: stabldev Date: Wed, 18 Jun 2025 19:00:05 +0530 Subject: [PATCH 05/14] fix: type --- src/app/(app)/~/[repo]/new/_components/field-item.tsx | 10 ++++++---- src/app/(app)/~/[repo]/new/_components/sidebar.tsx | 10 ++++++++-- src/app/(app)/~/[repo]/new/_lib/types.ts | 10 +++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/app/(app)/~/[repo]/new/_components/field-item.tsx b/src/app/(app)/~/[repo]/new/_components/field-item.tsx index d50d21f..2257aee 100644 --- a/src/app/(app)/~/[repo]/new/_components/field-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/field-item.tsx @@ -1,15 +1,17 @@ +import { ComponentsId } from '../_lib/types'; + interface Props { - id: string; + id: ComponentsId; label: string; Icon: React.ComponentType<{ className: string }>; - onClick: (id: string) => void; + addField: (id: ComponentsId) => void; } -export default function FieldItem({ id, label, Icon, onClick }: Props) { +export default function FieldItem({ id, label, Icon, addField }: Props) { return (
diff --git a/src/app/(app)/~/[repo]/new/_lib/types.ts b/src/app/(app)/~/[repo]/new/_lib/types.ts index 844627c..3acfb16 100644 --- a/src/app/(app)/~/[repo]/new/_lib/types.ts +++ b/src/app/(app)/~/[repo]/new/_lib/types.ts @@ -1,8 +1,3 @@ -export interface ActiveField { - id: string; - componentId: string; -} - export type ComponentsId = 'text' | 'slug'; export type Components = Record< ComponentsId, @@ -11,3 +6,8 @@ export type Components = Record< Icon: React.ComponentType<{ className: string }>; } >; + +export interface ActiveField { + id: string; + componentId: ComponentsId; +} From 78ef56d779a975301a94b651c175d71cb34f529f Mon Sep 17 00:00:00 2001 From: stabldev Date: Wed, 18 Jun 2025 19:34:51 +0530 Subject: [PATCH 06/14] chore: update elements --- src/app/(app)/~/[repo]/new/_components/field-item.tsx | 2 +- src/app/(app)/~/[repo]/new/_components/sidebar.tsx | 10 +++++++++- src/app/(app)/~/[repo]/new/_lib/constants.ts | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/(app)/~/[repo]/new/_components/field-item.tsx b/src/app/(app)/~/[repo]/new/_components/field-item.tsx index 2257aee..f0bbc4c 100644 --- a/src/app/(app)/~/[repo]/new/_components/field-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/field-item.tsx @@ -10,7 +10,7 @@ interface Props { export default function FieldItem({ id, label, Icon, addField }: Props) { return (
- Click on a field below to add +
+ Click on a field below to add + +
{Object.entries(components).map(([id, { label, Icon }]) => ( Date: Wed, 18 Jun 2025 19:45:43 +0530 Subject: [PATCH 07/14] chore: update header --- src/app/(app)/_components/header/header.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/(app)/_components/header/header.tsx b/src/app/(app)/_components/header/header.tsx index 4338f8a..0d10351 100644 --- a/src/app/(app)/_components/header/header.tsx +++ b/src/app/(app)/_components/header/header.tsx @@ -39,8 +39,9 @@ export default function Header() { )}
-
From b8714c74fc065c9cf9a092548bd5dbdae8a4a454 Mon Sep 17 00:00:00 2001 From: stabldev Date: Thu, 19 Jun 2025 09:40:29 +0530 Subject: [PATCH 08/14] feat: delete active field --- .../~/[repo]/new/_components/active-field-item.tsx | 13 +++++++++---- src/app/(app)/~/[repo]/new/_components/sidebar.tsx | 14 +++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx b/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx index b3befb5..e419664 100644 --- a/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx @@ -1,15 +1,16 @@ import { cn } from '@/lib/utils'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { GripVertical } from 'lucide-react'; +import { GripVertical, Trash } from 'lucide-react'; interface Props { id: string; label: string; Icon: React.ComponentType<{ className: string }>; + onDelete: (id: string) => void; } -export default function ActiveFieldItem({ id, label, Icon }: Props) { +export default function ActiveFieldItem({ id, label, Icon, onDelete }: Props) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, }); @@ -24,14 +25,18 @@ export default function ActiveFieldItem({ id, label, Icon }: Props) {
{label} + onDelete(id)} + /> [...prev, { id: `${componentId}-${nanoid()}`, componentId }]); } + function deleteField(id: string) { + setActiveFields((prev) => prev.filter((p) => p.id !== id)); + } + return (
@@ -46,7 +50,15 @@ export default function Sidebar({ activeFields, setActiveFields }: Props) { if (!component) return null; const { label, Icon } = components[component as ComponentsId]; - return ; + return ( + + ); })}
From 27f6f1aaab6b88467196e5223c8f5af610f4cb8e Mon Sep 17 00:00:00 2001 From: stabldev Date: Thu, 19 Jun 2025 16:20:20 +0530 Subject: [PATCH 09/14] chore: add --- .../new/_components/active-field-item.tsx | 2 +- .../(app)/~/[repo]/new/_components/editor.tsx | 18 ++++++++++++ .../~/[repo]/new/_components/field-item.tsx | 4 +-- .../new/_components/fields/text-input.tsx | 28 +++++++++++++++++++ .../~/[repo]/new/_components/sidebar.tsx | 17 +++++------ .../new/{_lib/constants.ts => _constants.ts} | 2 +- .../new/_contexts/active-field-context.ts | 9 ++++++ .../~/[repo]/new/_hooks/use-active-field.ts | 11 ++++++++ .../new/_providers/active-field-provider.tsx | 24 ++++++++++++++++ .../~/[repo]/new/{_lib/types.ts => _types.ts} | 2 ++ src/app/(app)/~/[repo]/new/page.tsx | 21 ++++---------- 11 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx rename src/app/(app)/~/[repo]/new/{_lib/constants.ts => _constants.ts} (85%) create mode 100644 src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts create mode 100644 src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts create mode 100644 src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx rename src/app/(app)/~/[repo]/new/{_lib/types.ts => _types.ts} (88%) diff --git a/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx b/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx index e419664..b81c197 100644 --- a/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/active-field-item.tsx @@ -28,7 +28,7 @@ export default function ActiveFieldItem({ id, label, Icon, onDelete }: Props) { className="bg-secondary/50 group hover:bg-secondary flex touch-none items-center gap-2 rounded-md p-2 transition-colors select-none" {...attributes} > - + {label}
@@ -7,6 +13,18 @@ export default function Editor() { Arrange and edit frontmatter fields below
+
+ {activeFields.map((field) => ( + + ))} +
); } + +function RenderField({ componentId }: { componentId: ComponentsId }) { + switch (componentId) { + case 'text': + return ; + } +} diff --git a/src/app/(app)/~/[repo]/new/_components/field-item.tsx b/src/app/(app)/~/[repo]/new/_components/field-item.tsx index f0bbc4c..aedbe05 100644 --- a/src/app/(app)/~/[repo]/new/_components/field-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/field-item.tsx @@ -1,4 +1,4 @@ -import { ComponentsId } from '../_lib/types'; +import { ComponentsId } from '../_types'; interface Props { id: ComponentsId; @@ -13,7 +13,7 @@ export default function FieldItem({ id, label, Icon, addField }: Props) { className="hover:bg-secondary/50 flex touch-none items-center gap-2 rounded-md p-2 transition-colors select-none" onClick={() => addField(id)} > - + {label} ); diff --git a/src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx b/src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx new file mode 100644 index 0000000..7218686 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx @@ -0,0 +1,28 @@ +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { GripVertical, RotateCcw, Trash, Type } from 'lucide-react'; + +export default function TextInput() { + return ( +
+ + + : + +
+ + + +
+
+ ); +} diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index 3fb3cf8..9be89bc 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -2,17 +2,15 @@ import { DndContext, DragEndEvent } from '@dnd-kit/core'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { nanoid } from 'nanoid'; -import { components } from '../_lib/constants'; -import { ActiveField, ComponentsId } from '../_lib/types'; +import { components } from '../_constants'; +import { useActiveField } from '../_hooks/use-active-field'; +import { ComponentsId } from '../_types'; import ActiveFieldItem from './active-field-item'; import FieldItem from './field-item'; -interface Props { - activeFields: ActiveField[]; - setActiveFields: React.Dispatch>; -} +export default function Sidebar() { + const { activeFields, setActiveFields } = useActiveField(); -export default function Sidebar({ activeFields, setActiveFields }: Props) { function handleDragEnd(e: DragEndEvent) { const { active, over } = e; @@ -29,7 +27,10 @@ export default function Sidebar({ activeFields, setActiveFields }: Props) { } function addField(componentId: ComponentsId) { - setActiveFields((prev) => [...prev, { id: `${componentId}-${nanoid()}`, componentId }]); + setActiveFields((prev) => [ + ...prev, + { id: `${componentId}-${nanoid()}`, componentId, key: '', value: '' }, + ]); } function deleteField(id: string) { diff --git a/src/app/(app)/~/[repo]/new/_lib/constants.ts b/src/app/(app)/~/[repo]/new/_constants.ts similarity index 85% rename from src/app/(app)/~/[repo]/new/_lib/constants.ts rename to src/app/(app)/~/[repo]/new/_constants.ts index 66447af..a0cad41 100644 --- a/src/app/(app)/~/[repo]/new/_lib/constants.ts +++ b/src/app/(app)/~/[repo]/new/_constants.ts @@ -1,5 +1,5 @@ import { TextCursorInput, Type } from 'lucide-react'; -import { Components } from './types'; +import { Components } from './_types'; export const components: Components = { text: { diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts b/src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts new file mode 100644 index 0000000..bfc09b8 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react'; +import { ActiveField } from '../_types'; + +export interface ActiveFieldContextValue { + activeFields: ActiveField[]; + setActiveFields: React.Dispatch>; +} + +export const ActiveFieldContext = createContext(null); diff --git a/src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts b/src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts new file mode 100644 index 0000000..9dc3344 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react'; +import { ActiveFieldContext, ActiveFieldContextValue } from '../_contexts/active-field-context'; + +export function useActiveField(): ActiveFieldContextValue { + const context = useContext(ActiveFieldContext); + if (!context) { + throw new Error('useField must be used within FieldProvider'); + } + + return context; +} diff --git a/src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx b/src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx new file mode 100644 index 0000000..807f478 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx @@ -0,0 +1,24 @@ +import { nanoid } from 'nanoid'; +import { useState } from 'react'; +import { + ActiveFieldContext, + type ActiveFieldContextValue, +} from '../_contexts/active-field-context'; +import { ActiveField } from '../_types'; + +interface FieldProviderProps { + children: React.ReactNode; +} + +export function FieldProvider({ children }: FieldProviderProps) { + const [activeFields, setActiveFields] = useState([ + { id: `text-${nanoid()}`, componentId: 'text', key: '', value: '' }, + ]); + + const contextValue: ActiveFieldContextValue = { + activeFields, + setActiveFields, + }; + + return {children}; +} diff --git a/src/app/(app)/~/[repo]/new/_lib/types.ts b/src/app/(app)/~/[repo]/new/_types.ts similarity index 88% rename from src/app/(app)/~/[repo]/new/_lib/types.ts rename to src/app/(app)/~/[repo]/new/_types.ts index 3acfb16..1c6dff8 100644 --- a/src/app/(app)/~/[repo]/new/_lib/types.ts +++ b/src/app/(app)/~/[repo]/new/_types.ts @@ -10,4 +10,6 @@ export type Components = Record< export interface ActiveField { id: string; componentId: ComponentsId; + key: string; + value: string; } diff --git a/src/app/(app)/~/[repo]/new/page.tsx b/src/app/(app)/~/[repo]/new/page.tsx index ddedb65..31de78e 100644 --- a/src/app/(app)/~/[repo]/new/page.tsx +++ b/src/app/(app)/~/[repo]/new/page.tsx @@ -1,27 +1,18 @@ 'use client'; -import { nanoid } from 'nanoid'; -import { useState } from 'react'; import Editor from './_components/editor'; import Sidebar from './_components/sidebar'; -import { ActiveField } from './_lib/types'; +import { FieldProvider } from './_providers/active-field-provider'; export default function Page() { - const [activeFields, setActiveFields] = useState>([ - { id: `text-${nanoid()}`, componentId: 'text' }, - ]); - return ( - <> - {/* adding title like this cause of client component */} - New - {/* rest of the layout */} -
+
+
- +
-
- + +
); } From 075582e380640bf59949c43a432a3f417106d1f5 Mon Sep 17 00:00:00 2001 From: stabldev Date: Thu, 19 Jun 2025 16:38:59 +0530 Subject: [PATCH 10/14] chore: add --- .../~/[repo]/new/_components/sidebar.tsx | 2 +- .../new/_contexts/active-field-context.ts | 9 ----- .../~/[repo]/new/_contexts/active-field.tsx | 37 +++++++++++++++++++ .../~/[repo]/new/_hooks/use-active-field.ts | 11 ------ .../new/_providers/active-field-provider.tsx | 24 ------------ src/app/(app)/~/[repo]/new/page.tsx | 6 +-- 6 files changed, 41 insertions(+), 48 deletions(-) delete mode 100644 src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts create mode 100644 src/app/(app)/~/[repo]/new/_contexts/active-field.tsx delete mode 100644 src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts delete mode 100644 src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index 9be89bc..e507886 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -3,7 +3,7 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { nanoid } from 'nanoid'; import { components } from '../_constants'; -import { useActiveField } from '../_hooks/use-active-field'; +import { useActiveField } from '../_contexts/active-field'; import { ComponentsId } from '../_types'; import ActiveFieldItem from './active-field-item'; import FieldItem from './field-item'; diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts b/src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts deleted file mode 100644 index bfc09b8..0000000 --- a/src/app/(app)/~/[repo]/new/_contexts/active-field-context.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createContext } from 'react'; -import { ActiveField } from '../_types'; - -export interface ActiveFieldContextValue { - activeFields: ActiveField[]; - setActiveFields: React.Dispatch>; -} - -export const ActiveFieldContext = createContext(null); diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field.tsx b/src/app/(app)/~/[repo]/new/_contexts/active-field.tsx new file mode 100644 index 0000000..cbb36e5 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_contexts/active-field.tsx @@ -0,0 +1,37 @@ +import { nanoid } from 'nanoid'; +import { createContext, useContext, useState } from 'react'; +import { ActiveField } from '../_types'; + +// ===== context ===== +interface ActiveFieldContextValue { + activeFields: ActiveField[]; + setActiveFields: React.Dispatch>; +} + +export const ActiveFieldContext = createContext(null); + +interface ActiveFieldProviderProps { + children: React.ReactNode; +} + +// ===== provider ===== +export function ActiveFieldProvider({ children }: ActiveFieldProviderProps) { + const [activeFields, setActiveFields] = useState([ + { id: `text-${nanoid()}`, componentId: 'text', key: '', value: '' }, + ]); + + const contextValue: ActiveFieldContextValue = { + activeFields, + setActiveFields, + }; + + return {children}; +} + +// ===== hook ===== +export function useActiveField(): ActiveFieldContextValue { + const context = useContext(ActiveFieldContext); + if (!context) throw new Error('useField must be used within FieldProvider'); + + return context; +} diff --git a/src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts b/src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts deleted file mode 100644 index 9dc3344..0000000 --- a/src/app/(app)/~/[repo]/new/_hooks/use-active-field.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; -import { ActiveFieldContext, ActiveFieldContextValue } from '../_contexts/active-field-context'; - -export function useActiveField(): ActiveFieldContextValue { - const context = useContext(ActiveFieldContext); - if (!context) { - throw new Error('useField must be used within FieldProvider'); - } - - return context; -} diff --git a/src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx b/src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx deleted file mode 100644 index 807f478..0000000 --- a/src/app/(app)/~/[repo]/new/_providers/active-field-provider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { nanoid } from 'nanoid'; -import { useState } from 'react'; -import { - ActiveFieldContext, - type ActiveFieldContextValue, -} from '../_contexts/active-field-context'; -import { ActiveField } from '../_types'; - -interface FieldProviderProps { - children: React.ReactNode; -} - -export function FieldProvider({ children }: FieldProviderProps) { - const [activeFields, setActiveFields] = useState([ - { id: `text-${nanoid()}`, componentId: 'text', key: '', value: '' }, - ]); - - const contextValue: ActiveFieldContextValue = { - activeFields, - setActiveFields, - }; - - return {children}; -} diff --git a/src/app/(app)/~/[repo]/new/page.tsx b/src/app/(app)/~/[repo]/new/page.tsx index 31de78e..eacb44f 100644 --- a/src/app/(app)/~/[repo]/new/page.tsx +++ b/src/app/(app)/~/[repo]/new/page.tsx @@ -2,17 +2,17 @@ import Editor from './_components/editor'; import Sidebar from './_components/sidebar'; -import { FieldProvider } from './_providers/active-field-provider'; +import { ActiveFieldProvider } from './_contexts/active-field'; export default function Page() { return (
- +
-
+
); } From 892ca73b7b04ab0d291b8258fb1adb01c0fe930f Mon Sep 17 00:00:00 2001 From: stabldev Date: Thu, 19 Jun 2025 16:52:51 +0530 Subject: [PATCH 11/14] chore: provider file --- .../~/[repo]/new/_components/sidebar.tsx | 2 +- .../new/_contexts/active-field.context.ts | 15 ++++++++ .../new/_contexts/active-field.provider.tsx | 21 +++++++++++ .../~/[repo]/new/_contexts/active-field.tsx | 37 ------------------- src/app/(app)/~/[repo]/new/page.tsx | 2 +- 5 files changed, 38 insertions(+), 39 deletions(-) create mode 100644 src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts create mode 100644 src/app/(app)/~/[repo]/new/_contexts/active-field.provider.tsx delete mode 100644 src/app/(app)/~/[repo]/new/_contexts/active-field.tsx diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index e507886..011c0f7 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -3,7 +3,7 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { nanoid } from 'nanoid'; import { components } from '../_constants'; -import { useActiveField } from '../_contexts/active-field'; +import { useActiveField } from '../_contexts/active-field.context'; import { ComponentsId } from '../_types'; import ActiveFieldItem from './active-field-item'; import FieldItem from './field-item'; diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts b/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts new file mode 100644 index 0000000..e80eb68 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; +import { ActiveField } from '../_types'; + +export interface ActiveFieldContextValue { + activeFields: ActiveField[]; + setActiveFields: React.Dispatch>; +} + +export const ActiveFieldContext = createContext(null); +export function useActiveField(): ActiveFieldContextValue { + const context = useContext(ActiveFieldContext); + if (!context) throw new Error('useField must be used within FieldProvider'); + + return context; +} diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field.provider.tsx b/src/app/(app)/~/[repo]/new/_contexts/active-field.provider.tsx new file mode 100644 index 0000000..a04fcf1 --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_contexts/active-field.provider.tsx @@ -0,0 +1,21 @@ +import { nanoid } from 'nanoid'; +import { useState } from 'react'; +import { ActiveField } from '../_types'; +import { ActiveFieldContext, ActiveFieldContextValue } from './active-field.context'; + +interface ActiveFieldProviderProps { + children: React.ReactNode; +} + +export function ActiveFieldProvider({ children }: ActiveFieldProviderProps) { + const [activeFields, setActiveFields] = useState([ + { id: `text-${nanoid()}`, componentId: 'text', key: '', value: '' }, + ]); + + const contextValue: ActiveFieldContextValue = { + activeFields, + setActiveFields, + }; + + return {children}; +} diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field.tsx b/src/app/(app)/~/[repo]/new/_contexts/active-field.tsx deleted file mode 100644 index cbb36e5..0000000 --- a/src/app/(app)/~/[repo]/new/_contexts/active-field.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { nanoid } from 'nanoid'; -import { createContext, useContext, useState } from 'react'; -import { ActiveField } from '../_types'; - -// ===== context ===== -interface ActiveFieldContextValue { - activeFields: ActiveField[]; - setActiveFields: React.Dispatch>; -} - -export const ActiveFieldContext = createContext(null); - -interface ActiveFieldProviderProps { - children: React.ReactNode; -} - -// ===== provider ===== -export function ActiveFieldProvider({ children }: ActiveFieldProviderProps) { - const [activeFields, setActiveFields] = useState([ - { id: `text-${nanoid()}`, componentId: 'text', key: '', value: '' }, - ]); - - const contextValue: ActiveFieldContextValue = { - activeFields, - setActiveFields, - }; - - return {children}; -} - -// ===== hook ===== -export function useActiveField(): ActiveFieldContextValue { - const context = useContext(ActiveFieldContext); - if (!context) throw new Error('useField must be used within FieldProvider'); - - return context; -} diff --git a/src/app/(app)/~/[repo]/new/page.tsx b/src/app/(app)/~/[repo]/new/page.tsx index eacb44f..8c75ff1 100644 --- a/src/app/(app)/~/[repo]/new/page.tsx +++ b/src/app/(app)/~/[repo]/new/page.tsx @@ -2,7 +2,7 @@ import Editor from './_components/editor'; import Sidebar from './_components/sidebar'; -import { ActiveFieldProvider } from './_contexts/active-field'; +import { ActiveFieldProvider } from './_contexts/active-field.context'; export default function Page() { return ( From c634549ea8ea2bcd17fc38d915a63c3a738f4a34 Mon Sep 17 00:00:00 2001 From: stabldev Date: Thu, 19 Jun 2025 16:58:40 +0530 Subject: [PATCH 12/14] chore: add _providers dir and rename files --- src/app/(app)/~/[repo]/new/_components/editor.tsx | 4 ++-- src/app/(app)/~/[repo]/new/_components/field-item.tsx | 2 +- src/app/(app)/~/[repo]/new/_components/sidebar.tsx | 4 ++-- src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts | 2 +- .../new/{_contexts => _providers}/active-field.provider.tsx | 4 ++-- src/app/(app)/~/[repo]/new/{_constants.ts => constants.ts} | 2 +- src/app/(app)/~/[repo]/new/page.tsx | 2 +- src/app/(app)/~/[repo]/new/{_types.ts => types.ts} | 0 8 files changed, 10 insertions(+), 10 deletions(-) rename src/app/(app)/~/[repo]/new/{_contexts => _providers}/active-field.provider.tsx (80%) rename src/app/(app)/~/[repo]/new/{_constants.ts => constants.ts} (85%) rename src/app/(app)/~/[repo]/new/{_types.ts => types.ts} (100%) diff --git a/src/app/(app)/~/[repo]/new/_components/editor.tsx b/src/app/(app)/~/[repo]/new/_components/editor.tsx index a731f91..dffcda5 100644 --- a/src/app/(app)/~/[repo]/new/_components/editor.tsx +++ b/src/app/(app)/~/[repo]/new/_components/editor.tsx @@ -1,5 +1,5 @@ -import { useActiveField } from '../_hooks/use-active-field'; -import { ComponentsId } from '../_types'; +import { useActiveField } from '../_contexts/active-field.context'; +import { ComponentsId } from '../types'; import TextInput from './fields/text-input'; export default function Editor() { diff --git a/src/app/(app)/~/[repo]/new/_components/field-item.tsx b/src/app/(app)/~/[repo]/new/_components/field-item.tsx index aedbe05..213f874 100644 --- a/src/app/(app)/~/[repo]/new/_components/field-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/field-item.tsx @@ -1,4 +1,4 @@ -import { ComponentsId } from '../_types'; +import { ComponentsId } from '../types'; interface Props { id: ComponentsId; diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index 011c0f7..7498df3 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -2,9 +2,9 @@ import { DndContext, DragEndEvent } from '@dnd-kit/core'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { nanoid } from 'nanoid'; -import { components } from '../_constants'; import { useActiveField } from '../_contexts/active-field.context'; -import { ComponentsId } from '../_types'; +import { components } from '../constants'; +import { ComponentsId } from '../types'; import ActiveFieldItem from './active-field-item'; import FieldItem from './field-item'; diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts b/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts index e80eb68..48577ab 100644 --- a/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts +++ b/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts @@ -1,5 +1,5 @@ import { createContext, useContext } from 'react'; -import { ActiveField } from '../_types'; +import { ActiveField } from '../types'; export interface ActiveFieldContextValue { activeFields: ActiveField[]; diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field.provider.tsx b/src/app/(app)/~/[repo]/new/_providers/active-field.provider.tsx similarity index 80% rename from src/app/(app)/~/[repo]/new/_contexts/active-field.provider.tsx rename to src/app/(app)/~/[repo]/new/_providers/active-field.provider.tsx index a04fcf1..b664a9e 100644 --- a/src/app/(app)/~/[repo]/new/_contexts/active-field.provider.tsx +++ b/src/app/(app)/~/[repo]/new/_providers/active-field.provider.tsx @@ -1,7 +1,7 @@ import { nanoid } from 'nanoid'; import { useState } from 'react'; -import { ActiveField } from '../_types'; -import { ActiveFieldContext, ActiveFieldContextValue } from './active-field.context'; +import { ActiveFieldContext, ActiveFieldContextValue } from '../_contexts/active-field.context'; +import { ActiveField } from '../types'; interface ActiveFieldProviderProps { children: React.ReactNode; diff --git a/src/app/(app)/~/[repo]/new/_constants.ts b/src/app/(app)/~/[repo]/new/constants.ts similarity index 85% rename from src/app/(app)/~/[repo]/new/_constants.ts rename to src/app/(app)/~/[repo]/new/constants.ts index a0cad41..66447af 100644 --- a/src/app/(app)/~/[repo]/new/_constants.ts +++ b/src/app/(app)/~/[repo]/new/constants.ts @@ -1,5 +1,5 @@ import { TextCursorInput, Type } from 'lucide-react'; -import { Components } from './_types'; +import { Components } from './types'; export const components: Components = { text: { diff --git a/src/app/(app)/~/[repo]/new/page.tsx b/src/app/(app)/~/[repo]/new/page.tsx index 8c75ff1..cc64146 100644 --- a/src/app/(app)/~/[repo]/new/page.tsx +++ b/src/app/(app)/~/[repo]/new/page.tsx @@ -2,7 +2,7 @@ import Editor from './_components/editor'; import Sidebar from './_components/sidebar'; -import { ActiveFieldProvider } from './_contexts/active-field.context'; +import { ActiveFieldProvider } from './_providers/active-field.provider'; export default function Page() { return ( diff --git a/src/app/(app)/~/[repo]/new/_types.ts b/src/app/(app)/~/[repo]/new/types.ts similarity index 100% rename from src/app/(app)/~/[repo]/new/_types.ts rename to src/app/(app)/~/[repo]/new/types.ts From b730195a7398f51165f21440961bf853cbc63ae4 Mon Sep 17 00:00:00 2001 From: stabldev Date: Thu, 19 Jun 2025 17:25:38 +0530 Subject: [PATCH 13/14] chore: update types --- .../(app)/~/[repo]/new/_components/editor.tsx | 8 +++---- .../~/[repo]/new/_components/field-item.tsx | 6 ++--- .../~/[repo]/new/_components/sidebar.tsx | 24 +++++++------------ .../new/_contexts/active-field.context.ts | 2 +- .../new/_providers/active-field.provider.tsx | 4 ++-- .../[repo]/new/{types.ts => _types/field.ts} | 8 +++---- src/app/(app)/~/[repo]/new/constants.ts | 4 ++-- 7 files changed, 25 insertions(+), 31 deletions(-) rename src/app/(app)/~/[repo]/new/{types.ts => _types/field.ts} (57%) diff --git a/src/app/(app)/~/[repo]/new/_components/editor.tsx b/src/app/(app)/~/[repo]/new/_components/editor.tsx index dffcda5..fc2debd 100644 --- a/src/app/(app)/~/[repo]/new/_components/editor.tsx +++ b/src/app/(app)/~/[repo]/new/_components/editor.tsx @@ -1,5 +1,5 @@ import { useActiveField } from '../_contexts/active-field.context'; -import { ComponentsId } from '../types'; +import { FieldId } from '../_types/field'; import TextInput from './fields/text-input'; export default function Editor() { @@ -15,15 +15,15 @@ export default function Editor() {
{activeFields.map((field) => ( - + ))}
); } -function RenderField({ componentId }: { componentId: ComponentsId }) { - switch (componentId) { +function RenderField({ fieldId }: { fieldId: FieldId }) { + switch (fieldId) { case 'text': return ; } diff --git a/src/app/(app)/~/[repo]/new/_components/field-item.tsx b/src/app/(app)/~/[repo]/new/_components/field-item.tsx index 213f874..59ccc84 100644 --- a/src/app/(app)/~/[repo]/new/_components/field-item.tsx +++ b/src/app/(app)/~/[repo]/new/_components/field-item.tsx @@ -1,10 +1,10 @@ -import { ComponentsId } from '../types'; +import { FieldId } from '../_types/field'; interface Props { - id: ComponentsId; + id: FieldId; label: string; Icon: React.ComponentType<{ className: string }>; - addField: (id: ComponentsId) => void; + addField: (id: FieldId) => void; } export default function FieldItem({ id, label, Icon, addField }: Props) { diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index 7498df3..18aaefb 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -3,8 +3,8 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { nanoid } from 'nanoid'; import { useActiveField } from '../_contexts/active-field.context'; -import { components } from '../constants'; -import { ComponentsId } from '../types'; +import { FieldId } from '../_types/field'; +import { FIELD } from '../constants'; import ActiveFieldItem from './active-field-item'; import FieldItem from './field-item'; @@ -26,10 +26,10 @@ export default function Sidebar() { } } - function addField(componentId: ComponentsId) { + function addField(fieldId: FieldId) { setActiveFields((prev) => [ ...prev, - { id: `${componentId}-${nanoid()}`, componentId, key: '', value: '' }, + { id: `${fieldId}-${nanoid()}`, fieldId: fieldId, key: '', value: '' }, ]); } @@ -46,11 +46,11 @@ export default function Sidebar() { Click on a field below to edit
- {activeFields.map(({ id, componentId }) => { - const component = Object.keys(components).find((c) => c === componentId); + {activeFields.map(({ id, fieldId: fieldId }) => { + const component = Object.keys(FIELD).find((c) => c === fieldId); if (!component) return null; - const { label, Icon } = components[component as ComponentsId]; + const { label, Icon } = FIELD[component as FieldId]; return ( Click on a field below to add
- {Object.entries(components).map(([id, { label, Icon }]) => ( - + {Object.entries(FIELD).map(([id, { label, Icon }]) => ( + ))}
diff --git a/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts b/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts index 48577ab..66ab3ce 100644 --- a/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts +++ b/src/app/(app)/~/[repo]/new/_contexts/active-field.context.ts @@ -1,5 +1,5 @@ import { createContext, useContext } from 'react'; -import { ActiveField } from '../types'; +import { ActiveField } from '../_types/field'; export interface ActiveFieldContextValue { activeFields: ActiveField[]; diff --git a/src/app/(app)/~/[repo]/new/_providers/active-field.provider.tsx b/src/app/(app)/~/[repo]/new/_providers/active-field.provider.tsx index b664a9e..4fa5583 100644 --- a/src/app/(app)/~/[repo]/new/_providers/active-field.provider.tsx +++ b/src/app/(app)/~/[repo]/new/_providers/active-field.provider.tsx @@ -1,7 +1,7 @@ import { nanoid } from 'nanoid'; import { useState } from 'react'; import { ActiveFieldContext, ActiveFieldContextValue } from '../_contexts/active-field.context'; -import { ActiveField } from '../types'; +import { ActiveField } from '../_types/field'; interface ActiveFieldProviderProps { children: React.ReactNode; @@ -9,7 +9,7 @@ interface ActiveFieldProviderProps { export function ActiveFieldProvider({ children }: ActiveFieldProviderProps) { const [activeFields, setActiveFields] = useState([ - { id: `text-${nanoid()}`, componentId: 'text', key: '', value: '' }, + { id: `text-${nanoid()}`, fieldId: 'text', key: '', value: '' }, ]); const contextValue: ActiveFieldContextValue = { diff --git a/src/app/(app)/~/[repo]/new/types.ts b/src/app/(app)/~/[repo]/new/_types/field.ts similarity index 57% rename from src/app/(app)/~/[repo]/new/types.ts rename to src/app/(app)/~/[repo]/new/_types/field.ts index 1c6dff8..7a371ce 100644 --- a/src/app/(app)/~/[repo]/new/types.ts +++ b/src/app/(app)/~/[repo]/new/_types/field.ts @@ -1,6 +1,6 @@ -export type ComponentsId = 'text' | 'slug'; -export type Components = Record< - ComponentsId, +export type FieldId = 'text' | 'slug'; +export type Field = Record< + FieldId, { label: string; Icon: React.ComponentType<{ className: string }>; @@ -9,7 +9,7 @@ export type Components = Record< export interface ActiveField { id: string; - componentId: ComponentsId; + fieldId: FieldId; key: string; value: string; } diff --git a/src/app/(app)/~/[repo]/new/constants.ts b/src/app/(app)/~/[repo]/new/constants.ts index 66447af..e767398 100644 --- a/src/app/(app)/~/[repo]/new/constants.ts +++ b/src/app/(app)/~/[repo]/new/constants.ts @@ -1,7 +1,7 @@ import { TextCursorInput, Type } from 'lucide-react'; -import { Components } from './types'; +import { Field } from './_types/field'; -export const components: Components = { +export const FIELD: Field = { text: { label: 'Text Input', Icon: Type, From f56ce2c05695630ac40f6946df0cddc30e2cdb48 Mon Sep 17 00:00:00 2001 From: stabldev Date: Thu, 19 Jun 2025 20:01:17 +0530 Subject: [PATCH 14/14] feat: sortable editor fields --- .../~/[repo]/new/_components/dnd/sortable.tsx | 40 ++++++++++++++ .../(app)/~/[repo]/new/_components/editor.tsx | 55 ++++++++++++++----- .../new/_components/fields/text-input.tsx | 23 +++++--- .../~/[repo]/new/_components/sidebar.tsx | 55 ++----------------- 4 files changed, 103 insertions(+), 70 deletions(-) create mode 100644 src/app/(app)/~/[repo]/new/_components/dnd/sortable.tsx diff --git a/src/app/(app)/~/[repo]/new/_components/dnd/sortable.tsx b/src/app/(app)/~/[repo]/new/_components/dnd/sortable.tsx new file mode 100644 index 0000000..1ab5f8e --- /dev/null +++ b/src/app/(app)/~/[repo]/new/_components/dnd/sortable.tsx @@ -0,0 +1,40 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { HTMLAttributes } from 'react'; + +interface Props { + id: string; + children: (props: { gripProps: HTMLAttributes }) => React.ReactNode; +} + +export default function Sortable({ id, children }: Props) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id, + }); + + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + zIndex: isDragging ? 1 : 0, + }; + + return ( + // suppressing "...attributes" warning +
+ {children({ + gripProps: { + ...listeners, + className: `text-muted-foreground size-5 stroke-[1.5] ${ + isDragging ? 'cursor-grabbing' : 'cursor-grab' + }`, + }, + })} +
+ ); +} diff --git a/src/app/(app)/~/[repo]/new/_components/editor.tsx b/src/app/(app)/~/[repo]/new/_components/editor.tsx index fc2debd..67c5018 100644 --- a/src/app/(app)/~/[repo]/new/_components/editor.tsx +++ b/src/app/(app)/~/[repo]/new/_components/editor.tsx @@ -1,9 +1,34 @@ +import { DndContext, DragEndEvent } from '@dnd-kit/core'; +import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; +import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { useActiveField } from '../_contexts/active-field.context'; import { FieldId } from '../_types/field'; +import Sortable from './dnd/sortable'; import TextInput from './fields/text-input'; export default function Editor() { - const { activeFields } = useActiveField(); + const { activeFields, setActiveFields } = useActiveField(); + + function getField(fieldId: FieldId) { + switch (fieldId) { + case 'text': + return TextInput; + } + } + + function handleDragEnd(e: DragEndEvent) { + const { active, over } = e; + + if (over && active.id !== over.id) { + setActiveFields((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + + const newArr = arrayMove(items, oldIndex, newIndex); + return newArr; + }); + } + } return (
@@ -13,18 +38,22 @@ export default function Editor() { Arrange and edit frontmatter fields below
-
- {activeFields.map((field) => ( - - ))} -
+ + +
+ {activeFields.map(({ id, fieldId }) => { + const field = getField(fieldId); + if (!field) return null; + + return ( + + {({ gripProps }) => field({ id, gripProps })} + + ); + })} +
+
+
); } - -function RenderField({ fieldId }: { fieldId: FieldId }) { - switch (fieldId) { - case 'text': - return ; - } -} diff --git a/src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx b/src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx index 7218686..62cffa5 100644 --- a/src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx +++ b/src/app/(app)/~/[repo]/new/_components/fields/text-input.tsx @@ -1,17 +1,26 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { GripVertical, RotateCcw, Trash, Type } from 'lucide-react'; +import { HTMLAttributes } from 'react'; +import { useActiveField } from '../../_contexts/active-field.context'; + +interface Props { + id: string; + gripProps: HTMLAttributes; +} + +export default function TextInput({ id, gripProps }: Props) { + const { activeFields } = useActiveField(); + const field = activeFields.find((field) => field.id === id); + + if (!field) return null; -export default function TextInput() { return (
- + : - +
-
diff --git a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx index 18aaefb..e9501a2 100644 --- a/src/app/(app)/~/[repo]/new/_components/sidebar.tsx +++ b/src/app/(app)/~/[repo]/new/_components/sidebar.tsx @@ -1,30 +1,11 @@ -import { DndContext, DragEndEvent } from '@dnd-kit/core'; -import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; -import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { nanoid } from 'nanoid'; import { useActiveField } from '../_contexts/active-field.context'; import { FieldId } from '../_types/field'; import { FIELD } from '../constants'; -import ActiveFieldItem from './active-field-item'; import FieldItem from './field-item'; export default function Sidebar() { - const { activeFields, setActiveFields } = useActiveField(); - - function handleDragEnd(e: DragEndEvent) { - const { active, over } = e; - - if (over && active.id !== over.id) { - setActiveFields((items) => { - const oldIndex = items.findIndex((item) => item.id === active.id); - const newIndex = items.findIndex((item) => item.id === over.id); - - const newArr = arrayMove(items, oldIndex, newIndex); - console.log(newArr); - return newArr; - }); - } - } + const { setActiveFields } = useActiveField(); function addField(fieldId: FieldId) { setActiveFields((prev) => [ @@ -33,38 +14,12 @@ export default function Sidebar() { ]); } - function deleteField(id: string) { - setActiveFields((prev) => prev.filter((p) => p.id !== id)); - } - return (
- - -
-

Fields

- Click on a field below to edit -
-
- {activeFields.map(({ id, fieldId: fieldId }) => { - const component = Object.keys(FIELD).find((c) => c === fieldId); - if (!component) return null; - - const { label, Icon } = FIELD[component as FieldId]; - return ( - - ); - })} -
-
-
- Click on a field below to add +
+

Fields

+ Click on a field below to add +
{Object.entries(FIELD).map(([id, { label, Icon }]) => (