Skip to content

Commit bdd487a

Browse files
Presets (#11)
1 parent 78bc869 commit bdd487a

17 files changed

+943
-20
lines changed

config/structured-data.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@
99
// Add taxonomy handles here that should have structured data objects
1010
// Example: 'categories', 'tags'
1111
],
12-
];
12+
'presets' => [
13+
'enabled' => true,
14+
'default_presets' => ['website', 'organization', 'article', 'webpage', 'localbusiness'],
15+
'custom_preset_paths' => [],
16+
],
17+
];

resources/dist/build/assets/statamic-structured-data-61a769e2.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/dist/build/assets/statamic-structured-data-84529790.css

Lines changed: 0 additions & 1 deletion
This file was deleted.

resources/dist/build/assets/statamic-structured-data-959d3d78.js

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/dist/build/assets/statamic-structured-data-e538cc20.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

resources/dist/build/manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
"src": "resources/css/statamic-structured-data.css"
66
},
77
"resources/js/statamic-structured-data.css": {
8-
"file": "assets/statamic-structured-data-84529790.css",
8+
"file": "assets/statamic-structured-data-61a769e2.css",
99
"src": "resources/js/statamic-structured-data.css"
1010
},
1111
"resources/js/statamic-structured-data.js": {
1212
"css": [
13-
"assets/statamic-structured-data-84529790.css"
13+
"assets/statamic-structured-data-61a769e2.css"
1414
],
15-
"file": "assets/statamic-structured-data-e538cc20.js",
15+
"file": "assets/statamic-structured-data-959d3d78.js",
1616
"isEntry": true,
1717
"src": "resources/js/statamic-structured-data.js"
1818
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<template>
2+
<div
3+
v-if="visible"
4+
class="fixed inset-0 bg-gray-900 bg-opacity-75 flex justify-center items-center p-4"
5+
style="z-index: 9999;"
6+
@click.self="close"
7+
>
8+
<div class="bg-white dark:bg-gray-800 rounded-lg w-full max-w-xl flex flex-col shadow-2xl" style="max-height: 60vh;">
9+
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
10+
<h3 class="font-bold text-lg text-gray-900 dark:text-white">{{ __('Add Preset') }}</h3>
11+
<button
12+
@click="close"
13+
class="text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 text-2xl leading-none bg-transparent border-0 cursor-pointer transition-colors"
14+
>
15+
&times;
16+
</button>
17+
</div>
18+
19+
<div class="px-6 py-4 overflow-y-auto flex-1 min-h-0">
20+
<div v-if="!selectedPreset" class="preset-selection">
21+
<p class="text-gray-600 dark:text-gray-300 mb-4">{{ __('Choose a preset to add to your schema:') }}</p>
22+
23+
<div class="grid grid-cols-1 gap-3">
24+
<div
25+
v-for="preset in presets"
26+
:key="preset.name"
27+
@click="selectPreset(preset)"
28+
class="border border-gray-200 dark:border-gray-700 rounded-md p-3 cursor-pointer transition-all duration-200 hover:border-blue-500 hover:shadow-sm hover:shadow-blue-500/10 dark:hover:border-blue-400 bg-white dark:bg-gray-800"
29+
>
30+
<div class="flex justify-between items-start">
31+
<div class="flex-1">
32+
<h4 class="font-semibold text-gray-900 dark:text-white text-sm">{{ preset.name }}</h4>
33+
<p class="text-xs text-gray-600 dark:text-gray-300 mt-1">{{ preset.description }}</p>
34+
35+
<div class="mt-2">
36+
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">{{ __('Fields:') }}</div>
37+
<div class="flex flex-wrap gap-1">
38+
<span
39+
v-for="field in preset.schema.fields.slice(0, 3)"
40+
:key="field.key"
41+
class="bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 px-1.5 py-0.5 rounded text-[9px] font-medium"
42+
>
43+
{{ field.key }}
44+
</span>
45+
<span
46+
v-if="preset.schema.fields.length > 3"
47+
class="bg-gray-100 dark:bg-gray-600 text-gray-600 dark:text-gray-300 px-1.5 py-0.5 rounded text-[9px]"
48+
>
49+
+{{ preset.schema.fields.length - 3 }}
50+
</span>
51+
</div>
52+
</div>
53+
</div>
54+
<div class="bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-200 px-2 py-1 rounded text-xs font-medium ml-2">
55+
{{ preset.schema.specialProps.type }}
56+
</div>
57+
</div>
58+
</div>
59+
</div>
60+
</div>
61+
62+
<div v-else class="preset-actions">
63+
<div class="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded border border-blue-200 dark:border-blue-800">
64+
<h4 class="font-semibold text-blue-800 dark:text-blue-200">{{ selectedPreset.name }}</h4>
65+
<p class="text-sm text-blue-600 dark:text-blue-300">{{ selectedPreset.description }}</p>
66+
</div>
67+
68+
<div v-if="hasExistingSchemas" class="action-selection">
69+
<p class="text-gray-700 dark:text-gray-200 mb-4">{{ __('You have existing schemas. How would you like to add this preset?') }}</p>
70+
71+
<div class="flex flex-col gap-3">
72+
<button
73+
@click="handleAction('merge')"
74+
class="border border-gray-200 dark:border-gray-700 rounded-md p-4 text-left cursor-pointer transition-all duration-200 bg-white dark:bg-gray-800 w-full hover:border-green-500 hover:shadow-md hover:shadow-green-500/10 dark:hover:border-green-400"
75+
>
76+
<div class="flex flex-col gap-1">
77+
<div class="font-semibold text-gray-700 dark:text-gray-200">{{ __('Merge (Recommended)') }}</div>
78+
<div class="text-sm text-gray-600 dark:text-gray-300">{{ __('Add this preset as an additional schema alongside your existing ones') }}</div>
79+
</div>
80+
</button>
81+
82+
<button
83+
@click="handleAction('override')"
84+
class="border border-gray-200 dark:border-gray-700 rounded-md p-4 text-left cursor-pointer transition-all duration-200 bg-white dark:bg-gray-800 w-full hover:border-amber-500 hover:shadow-md hover:shadow-amber-500/10 dark:hover:border-amber-400"
85+
>
86+
<div class="flex flex-col gap-1">
87+
<div class="font-semibold text-gray-700 dark:text-gray-200">{{ __('Override') }}</div>
88+
<div class="text-sm text-gray-600 dark:text-gray-300">{{ __('Replace all existing schemas with this preset') }}</div>
89+
</div>
90+
</button>
91+
</div>
92+
</div>
93+
94+
<div v-else class="no-existing-schemas">
95+
<p class="text-gray-600 dark:text-gray-300 mb-4">{{ __('This preset will be added as your first schema.') }}</p>
96+
<button
97+
@click="handleAction('add')"
98+
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white border-0 rounded cursor-pointer font-medium transition-all duration-200 w-full"
99+
>
100+
{{ __('Add Preset') }}
101+
</button>
102+
</div>
103+
</div>
104+
</div>
105+
106+
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end flex-shrink-0">
107+
<button
108+
@click="goBack"
109+
v-if="selectedPreset"
110+
class="mr-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 cursor-pointer transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-600 text-sm"
111+
>
112+
{{ __('Back') }}
113+
</button>
114+
<button
115+
@click="close"
116+
class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 cursor-pointer transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-600 text-sm"
117+
>
118+
{{ __('Cancel') }}
119+
</button>
120+
</div>
121+
</div>
122+
</div>
123+
</template>
124+
125+
<script>
126+
export default {
127+
name: 'PresetModal',
128+
129+
props: {
130+
visible: {
131+
type: Boolean,
132+
default: false
133+
},
134+
presets: {
135+
type: Array,
136+
default: () => []
137+
},
138+
hasExistingSchemas: {
139+
type: Boolean,
140+
default: false
141+
}
142+
},
143+
144+
data() {
145+
return {
146+
selectedPreset: null
147+
}
148+
},
149+
150+
watch: {
151+
visible: {
152+
immediate: true,
153+
handler(newVal) {
154+
if (newVal) {
155+
this.lockBodyScroll();
156+
} else {
157+
this.unlockBodyScroll();
158+
this.selectedPreset = null;
159+
}
160+
}
161+
}
162+
},
163+
164+
beforeDestroy() {
165+
this.unlockBodyScroll();
166+
},
167+
168+
methods: {
169+
lockBodyScroll() {
170+
document.body.style.overflow = 'hidden';
171+
},
172+
173+
unlockBodyScroll() {
174+
document.body.style.overflow = '';
175+
},
176+
177+
close() {
178+
this.selectedPreset = null;
179+
this.$emit('close');
180+
},
181+
182+
selectPreset(preset) {
183+
this.selectedPreset = preset;
184+
},
185+
186+
goBack() {
187+
this.selectedPreset = null;
188+
},
189+
190+
handleAction(action) {
191+
this.$emit('preset-selected', {
192+
preset: this.selectedPreset,
193+
action: action
194+
});
195+
this.close();
196+
}
197+
}
198+
}
199+
</script>

resources/js/components/fieldtypes/StructuredDataBuilder.vue

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
<div class="px-4 py-2 bg-gray-50 border-b rounded-t-lg flex justify-between items-center">
66
<div class="flex items-center gap-2 cursor-pointer" @click="toggleSchema(schemaIndex)">
77
<div class="chevron" :class="{ 'chevron-up': !isSchemaCollapsed(schemaIndex) }"></div>
8-
<h3 class="font-bold text-lg">{{ __('Schema') }} {{ schemaIndex + 1 }}</h3>
8+
<h3 class="font-bold text-lg">
9+
<span v-if="schema.specialProps.type">{{ schema.specialProps.type }}</span>
10+
<span v-else>{{ __('Schema') }} {{ schemaIndex + 1 }}</span>
11+
</h3>
912
</div>
1013
<button
1114
v-if="schemas.length > 1"
@@ -175,6 +178,13 @@
175178

176179
<div class="flex gap-2 mt-4">
177180
<button class="btn-primary" @click="addSchema">{{ __('Add Schema') }}</button>
181+
<button
182+
v-if="presetsEnabled && presets.length > 0"
183+
class="btn-preset"
184+
@click="showPresetModal = true"
185+
>
186+
{{ __('Add Preset') }}
187+
</button>
178188
<button class="btn" @click="togglePreview">
179189
{{ showPreview ? __('Hide Preview') : __('Show Preview') }}
180190
</button>
@@ -184,11 +194,20 @@
184194
<pre class="bg-gray-50 p-4 rounded-lg overflow-x-auto">{{ preview }}</pre>
185195
</div>
186196
</div>
197+
198+
<preset-modal
199+
:visible="showPresetModal"
200+
:presets="presets"
201+
:has-existing-schemas="schemas.length > 0"
202+
@close="showPresetModal = false"
203+
@preset-selected="handlePresetSelected"
204+
/>
187205
</div>
188206
</template>
189207

190208
<script>
191209
import StructuredDataObject from '../StructuredDataObject.vue';
210+
import PresetModal from '../PresetModal.vue';
192211
import { formatSchemaJson } from '../../utils/schema';
193212
import draggable from 'vuedraggable';
194213
@@ -198,6 +217,7 @@ export default {
198217
199218
components: {
200219
'structured-data-object': StructuredDataObject,
220+
'preset-modal': PresetModal,
201221
draggable,
202222
},
203223
@@ -234,6 +254,7 @@ export default {
234254
fields: []
235255
}],
236256
showPreview: false,
257+
showPresetModal: false,
237258
collapsedSchemas: {}
238259
}
239260
},
@@ -269,6 +290,14 @@ export default {
269290
value: '@dataObject::' + term.slug
270291
};
271292
});
293+
},
294+
295+
presets() {
296+
return this.meta?.presets || [];
297+
},
298+
299+
presetsEnabled() {
300+
return this.meta?.presets_enabled || false;
272301
}
273302
},
274303
@@ -312,6 +341,7 @@ export default {
312341
schema.fields = fields;
313342
}
314343
},
344+
315345
moveFieldDown(index, schema) {
316346
if (index < schema.fields.length - 1) {
317347
const fields = [...schema.fields];
@@ -398,6 +428,20 @@ export default {
398428
this.schemas.splice(index, 1);
399429
},
400430
431+
handlePresetSelected(data) {
432+
const { preset, action } = data;
433+
434+
switch (action) {
435+
case 'merge':
436+
case 'add':
437+
this.schemas.push(JSON.parse(JSON.stringify(preset.schema)));
438+
break;
439+
case 'override':
440+
this.schemas = [JSON.parse(JSON.stringify(preset.schema))];
441+
break;
442+
}
443+
},
444+
401445
getFieldByInput(inputElement) {
402446
const fieldElement = inputElement.closest('.field');
403447
if (fieldElement) {
@@ -419,12 +463,19 @@ export default {
419463
.structured-data-builder {
420464
max-width: 800px;
421465
}
466+
422467
.btn-close {
423468
@apply px-2 py-1 text-gray-500 hover:text-gray-700;
424469
}
470+
425471
.btn {
426472
@apply bg-gray-200 px-3 py-1 rounded hover:bg-gray-300;
427473
}
474+
475+
.btn-preset {
476+
@apply bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600 font-medium transition-colors;
477+
}
478+
428479
.chevron {
429480
width: 10px;
430481
height: 10px;
@@ -433,7 +484,8 @@ export default {
433484
transform: rotate(45deg);
434485
transition: transform 0.2s ease;
435486
}
487+
436488
.chevron-up {
437489
transform: rotate(-135deg);
438490
}
439-
</style>
491+
</style>

0 commit comments

Comments
 (0)