@portaki/module-sections 1.2.7 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@portaki/module-sections",
3
- "version": "1.2.7",
3
+ "version": "1.3.1",
4
4
  "description": "Portaki module — editorial sections (TipTap)",
5
5
  "license": "AGPL-3.0",
6
6
  "type": "module",
7
7
  "main": "./src/index.tsx",
8
8
  "dependencies": {
9
+ "@dnd-kit/core": "^6.3.1",
10
+ "@dnd-kit/sortable": "^10.0.0",
11
+ "@dnd-kit/utilities": "^3.2.2",
9
12
  "@portaki/sdk": "^3.0.0",
13
+ "lucide-react": "^1.14.0",
10
14
  "@tiptap/core": "^3.0.0",
11
15
  "@tiptap/html": "^3.0.0",
12
16
  "@tiptap/starter-kit": "^3.0.0",
@@ -9,7 +9,7 @@
9
9
  "fr": "Blocs de contenu riches (TipTap) pour le carnet d’accueil invité.",
10
10
  "en": "Rich content blocks (TipTap) for the guest welcome book."
11
11
  },
12
- "version": "1.2.7",
12
+ "version": "1.3.1",
13
13
  "releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
14
14
  "changelog": [
15
15
  {
@@ -122,6 +122,6 @@
122
122
  },
123
123
  "artifacts": {
124
124
  "wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
125
- "guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.2.7"
125
+ "guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.3.1"
126
126
  }
127
127
  }
@@ -9,7 +9,7 @@
9
9
  "fr": "Blocs de contenu riches (TipTap) pour le carnet d’accueil invité.",
10
10
  "en": "Rich content blocks (TipTap) for the guest welcome book."
11
11
  },
12
- "version": "1.2.7",
12
+ "version": "1.3.1",
13
13
  "releaseNotesUrl": "https://github.com/PortakiApp/portaki-modules/releases",
14
14
  "changelog": [
15
15
  {
@@ -44,7 +44,7 @@
44
44
  "portakiVersionMin": "1.0.0",
45
45
  "requiresHostSdk": "3.0.0",
46
46
  "database": {
47
- "schemaVersion": "1.0.1"
47
+ "schemaVersion": "1.0.2"
48
48
  },
49
49
  "license": "AGPL-3.0",
50
50
  "repository": "https://github.com/PortakiApp/portaki-modules",
@@ -122,6 +122,6 @@
122
122
  },
123
123
  "artifacts": {
124
124
  "wasmUrl": "oci://ghcr.io/portakiapp/portaki-module-sections",
125
- "guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.2.7"
125
+ "guestEsmUrl": "https://esm.sh/@portaki/module-sections@1.3.1"
126
126
  }
127
127
  }
@@ -16,10 +16,6 @@ type SectionRow = {
16
16
  contentEn: unknown
17
17
  }
18
18
 
19
- type ListResponse = {
20
- sections: SectionRow[]
21
- }
22
-
23
19
  const htmlExtensions = [StarterKit]
24
20
 
25
21
  function contentToHtml(raw: unknown): string {
@@ -33,7 +29,7 @@ function contentToHtml(raw: unknown): string {
33
29
  }
34
30
 
35
31
  export function SectionsGuestView({ lang }: { lang: LangCode }) {
36
- const { data, loading, error } = usePortakiQuery<ListResponse>('sections.list', {})
32
+ const { data, loading, error } = usePortakiQuery<SectionRow[]>('sections.list', {})
37
33
 
38
34
  if (loading) {
39
35
  return (
@@ -43,13 +39,13 @@ export function SectionsGuestView({ lang }: { lang: LangCode }) {
43
39
  )
44
40
  }
45
41
 
46
- if (error != null || !data?.sections?.length) {
42
+ if (error != null || !data?.length) {
47
43
  return null
48
44
  }
49
45
 
50
46
  return (
51
47
  <div className="space-y-10">
52
- {data.sections.map((section) => {
48
+ {data.map((section) => {
53
49
  const title = lang === 'en' ? section.titleEn || section.titleFr : section.titleFr
54
50
  const html = contentToHtml(lang === 'en' ? section.contentEn : section.contentFr)
55
51
  return (
@@ -0,0 +1,159 @@
1
+ 'use client'
2
+
3
+ import type { HostModuleContext } from '@portaki/sdk'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ import type {
7
+ SectionContentPresetId,
8
+ SectionCreateModalDesign,
9
+ SectionTemplateDesign,
10
+ } from './sections-host.types'
11
+
12
+ export type SectionCreatePayload = {
13
+ titleFr: string
14
+ titleEn: string
15
+ contentPreset: SectionContentPresetId
16
+ }
17
+
18
+ type Props = {
19
+ open: boolean
20
+ onOpenChange: (open: boolean) => void
21
+ design: SectionCreateModalDesign
22
+ onCreate: (payload: SectionCreatePayload) => Promise<void>
23
+ creating?: boolean
24
+ shell: HostModuleContext['shell']
25
+ }
26
+
27
+ export function SectionCreateModal({
28
+ open,
29
+ onOpenChange,
30
+ design,
31
+ onCreate,
32
+ creating = false,
33
+ shell,
34
+ }: Props) {
35
+ const {
36
+ Dialog,
37
+ DialogContent,
38
+ DialogDescription,
39
+ DialogFooter,
40
+ DialogHeader,
41
+ DialogTitle,
42
+ Button,
43
+ Input,
44
+ cn,
45
+ } = shell
46
+
47
+ const defaultTemplate =
48
+ design.templates.find((t) => t.id === design.defaultTemplateId) ?? design.templates[0]
49
+
50
+ const [selectedId, setSelectedId] = useState(defaultTemplate?.id ?? '')
51
+ const [titleFr, setTitleFr] = useState(defaultTemplate?.titleFr ?? '')
52
+
53
+ const selected = design.templates.find((t) => t.id === selectedId) ?? defaultTemplate
54
+
55
+ useEffect(() => {
56
+ if (!open || !defaultTemplate) {
57
+ return
58
+ }
59
+ setSelectedId(defaultTemplate.id)
60
+ setTitleFr(defaultTemplate.titleFr)
61
+ }, [open, defaultTemplate])
62
+
63
+ function selectTemplate(template: SectionTemplateDesign) {
64
+ setSelectedId(template.id)
65
+ setTitleFr(template.titleFr)
66
+ }
67
+
68
+ async function handleSubmit() {
69
+ if (!selected || !titleFr.trim()) {
70
+ return
71
+ }
72
+ await onCreate({
73
+ titleFr: titleFr.trim(),
74
+ titleEn: selected.titleEn,
75
+ contentPreset: selected.contentPreset,
76
+ })
77
+ }
78
+
79
+ return (
80
+ <Dialog open={open} onOpenChange={onOpenChange}>
81
+ <DialogContent className="flex max-h-[min(92vh,calc(100%-80px))] max-w-[min(96vw,920px)] flex-col gap-0 overflow-hidden p-0">
82
+ <DialogHeader className="shrink-0 px-[26px] pb-4 pt-6">
83
+ <DialogTitle className="font-heading text-[22px] font-semibold tracking-tight">
84
+ {design.titleFr}
85
+ </DialogTitle>
86
+ <DialogDescription className="text-[13px] leading-relaxed text-[var(--text-muted)]">
87
+ {design.subtitleFr}
88
+ </DialogDescription>
89
+ </DialogHeader>
90
+
91
+ <div className="min-h-0 flex-1 overflow-y-auto px-[26px] pb-4">
92
+ <div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
93
+ {design.templates.map((template) => {
94
+ const active = template.id === selectedId
95
+ return (
96
+ <button
97
+ key={template.id}
98
+ type="button"
99
+ onClick={() => selectTemplate(template)}
100
+ className={cn(
101
+ 'rounded-xl border px-3 py-3 text-left transition-colors',
102
+ active
103
+ ? 'border-[var(--amber)] bg-[var(--color-amber-soft)] shadow-[inset_0_0_0_1px_rgba(245,158,11,0.35)]'
104
+ : 'border-[var(--border)] bg-white hover:border-[var(--border-md)] hover:bg-[var(--bg-muted)]/40',
105
+ )}
106
+ >
107
+ <span className="text-[22px] leading-none" aria-hidden>
108
+ {template.icon}
109
+ </span>
110
+ <p className="mt-2 font-heading text-[14px] font-semibold leading-snug text-[var(--text)]">
111
+ {template.titleFr}
112
+ </p>
113
+ <p className="mt-1 text-[12px] leading-snug text-[var(--text-muted)]">
114
+ {template.descriptionFr}
115
+ </p>
116
+ </button>
117
+ )
118
+ })}
119
+ </div>
120
+
121
+ <div className="mt-5">
122
+ <label className="mb-1.5 block text-[13px] font-medium text-[var(--text)]">
123
+ {design.titleFieldLabelFr}
124
+ </label>
125
+ <Input
126
+ value={titleFr}
127
+ onChange={(e) => setTitleFr(e.target.value)}
128
+ placeholder={selected?.titleFr}
129
+ className="h-11"
130
+ />
131
+ <p className="mt-1.5 text-[12px] text-[var(--text-muted)]">{design.titleFieldHintFr}</p>
132
+ </div>
133
+
134
+ <p className="mt-4 text-[12px] text-[var(--text-muted)]">{design.footerHintFr}</p>
135
+ </div>
136
+
137
+ <DialogFooter className="shrink-0 flex-row items-center justify-end gap-2 border-t border-[var(--border)] bg-[var(--bg-surface)] px-[26px] py-4">
138
+ <Button
139
+ type="button"
140
+ variant="ghost"
141
+ className="text-[var(--text-muted)]"
142
+ disabled={creating}
143
+ onClick={() => onOpenChange(false)}
144
+ >
145
+ {design.cancelLabelFr}
146
+ </Button>
147
+ <Button
148
+ type="button"
149
+ className="bg-[var(--amber)] font-semibold text-[var(--text)] hover:bg-[var(--amber-solid-hover)]"
150
+ disabled={creating || !titleFr.trim()}
151
+ onClick={() => void handleSubmit()}
152
+ >
153
+ {creating ? 'Création…' : design.submitLabelFr}
154
+ </Button>
155
+ </DialogFooter>
156
+ </DialogContent>
157
+ </Dialog>
158
+ )
159
+ }