Transferred and translated the frontend code from L10 to L12
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
<script setup>
|
||||
import { Head, Link, usePage, useForm } from '@inertiajs/vue3';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
|
||||
import blocks from '@/Utils/blocks';
|
||||
import PageBuilderLayout from '@/layouts/PageBuilderLayout.vue';
|
||||
import BlockGroup from '@/Components/Blocks/BlockGroup.vue';
|
||||
import BlockWrapper from '@/Components/Blocks/BlockWrapper.vue';
|
||||
import AppLogo from '@/Components/PageBuilderLogo.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
|
||||
const groups = blocks.groups;
|
||||
const draft = ref([]);
|
||||
const page = usePage();
|
||||
|
||||
onMounted(() => {
|
||||
initFlowbite();
|
||||
});
|
||||
|
||||
const { builtPages, images } = defineProps({
|
||||
builtPages: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
images: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
});
|
||||
|
||||
const cloneBlock = (block) => {
|
||||
block.uuid = uuidv4();
|
||||
return block;
|
||||
};
|
||||
|
||||
const deleteBlock = (index) => {
|
||||
draft.value.splice(index, 1);
|
||||
};
|
||||
|
||||
function handleDrop(event) {
|
||||
const data = event.dataTransfer.getData('application/json');
|
||||
if (data) {
|
||||
try {
|
||||
const block = JSON.parse(data);
|
||||
block.uuid = uuidv4();
|
||||
draft.value.push(block);
|
||||
console.log('📦 Dropped block:', block);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse dropped block", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Drag and drop
|
||||
|
||||
const draggingIndex = ref(null)
|
||||
|
||||
function handleDragStart(event, index) {
|
||||
draggingIndex.value = index;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', index);
|
||||
};
|
||||
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
function handleDropReorder(event, dropIndex) {
|
||||
event.preventDefault();
|
||||
const fromIndex = draggingIndex.value;
|
||||
if (fromIndex === null || fromIndex === dropIndex) return;
|
||||
|
||||
const block = draft.value.splice(fromIndex, 1)[0];
|
||||
draft.value.splice(dropIndex, 0, block);
|
||||
draggingIndex.value = null;
|
||||
};
|
||||
|
||||
// Save form
|
||||
const pageForm = useForm({
|
||||
title: '',
|
||||
content: '',
|
||||
publish: false,
|
||||
mainpage: false,
|
||||
linked: false,
|
||||
linkorder: 0,
|
||||
visibility: 'public'
|
||||
});
|
||||
|
||||
const savePage = () => {
|
||||
const dropdown = document.getElementById('dropdownInformation');
|
||||
if (dropdown) dropdown.classList.add('hidden');
|
||||
|
||||
pageForm.content = JSON.stringify(draft.value);
|
||||
pageForm.post(route('page-builder.landing-page.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
// onSuccess, the user is redirected to the index page for the time being, so toast handling has to be implemented there
|
||||
},
|
||||
onError: () => {
|
||||
console.warn("Valideringsfeil ved lagring.")
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Ny Side" />
|
||||
|
||||
<PageBuilderLayout>
|
||||
<template #header>
|
||||
<AppLogo />
|
||||
<h3 class="text-lg font-semibold">Ny side</h3>
|
||||
<div>
|
||||
|
||||
<button id="dropdownInformationButton" data-dropdown-toggle="dropdownInformation" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
|
||||
Handlinger
|
||||
<svg class="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div id="dropdownInformation" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
|
||||
<div class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
||||
<div>{{ page.props.auth.user.name }}</div>
|
||||
<div class="font-medium truncate">{{ page.props.auth.user.email }}</div>
|
||||
</div>
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownInformationButton">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="savePage"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>
|
||||
Lagre side
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="py-2">
|
||||
<Link :href="route('page-builder.index')" class="block px-4 py-2 btn btn-sm btn-ghost hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
Lukk
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Sidebar with form & block groups -->
|
||||
<div class="bg-slate-100 overflow-y-auto w-1/6 p-4 space-y-4">
|
||||
<div class="space-y-4 p-4 rounded border dark:border-gray-600 bg-gray-50 dark:bg-gray-800">
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="page_title-text"
|
||||
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Sidetittel
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="page_title-text"
|
||||
aria-describedby="page_title-text-explanation"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Skriv inn en tittel"
|
||||
v-model="pageForm.title"
|
||||
>
|
||||
<InputError :message="pageForm.errors.title" class="mt-2" />
|
||||
<p id="page_title-text-explanation" class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Tittelen på siden vil være synlig i fanearket i nettleseren hos besøkende, samt brukes til å generere URL (slug)
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="pageForm.linked" class="sr-only peer">
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Menylenke</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="steps-range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Menylenke rekkefølge</label>
|
||||
<input id="steps-range" :disabled="!pageForm.linked" v-model="pageForm.linkorder" type="range" min="0" max="20" step="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700">
|
||||
<span class="text-xs">Prioritet: {{ pageForm.linkorder }} (høyere verdi kommer først)</span>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="pageForm.publish" class="sr-only peer">
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Publisert</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="pageForm.mainpage" class="sr-only peer">
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Sett som hovedside</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center mb-4">
|
||||
<input :checked="pageForm.visibility === 'public'" id="page_state-radio-1" v-model="pageForm.visibility" type="radio" value="public" name="page_state-radio" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="page_state-radio-1" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Synlig for alle</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input :checked="pageForm.visibility === 'private'" id="page_state-radio-2" v-model="pageForm.visibility" type="radio" value="private" name="page_state-radio" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="page_state-radio-2" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Bare synlig for medlemmer</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BlockGroup
|
||||
v-for="group in groups"
|
||||
:key="group.uuid"
|
||||
:title="group.title"
|
||||
:blocks="group.blocks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Builder canvas -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 w-5/6 p-6 overflow-y-auto"
|
||||
@dragover.prevent
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<div v-if="draft.length === 0" class="text-gray-500 text-sm italic">
|
||||
Dra inn blokker fra venstre for å begynne.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(block, index) in draft"
|
||||
:key="block.uuid"
|
||||
class="mb-4"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, index)"
|
||||
@dragover="handleDragOver"
|
||||
@drop="handleDropReorder($event, index)"
|
||||
>
|
||||
<BlockWrapper :images="images" :block="block" @delete="() => deleteBlock(index)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBuilderLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* scoped styles for the create screen */
|
||||
</style>
|
||||
@@ -0,0 +1,623 @@
|
||||
<script setup>
|
||||
import { Head, Link, usePage, useForm, router } from '@inertiajs/vue3';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
import { showToast } from '@/Composables/useToast.js';
|
||||
|
||||
import blocks from '@/Utils/blocks';
|
||||
import PageBuilderLayout from '@/layouts/PageBuilderLayout.vue';
|
||||
import BlockGroup from '@/Components/Blocks/BlockGroup.vue';
|
||||
import BlockWrapper from '@/Components/Blocks/BlockWrapper.vue';
|
||||
import AppLogo from '@/Components/PageBuilderLogo.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import Modal from '@/Components/Modal.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import DangerButton from '@/Components/DangerButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
let initialContent = revision.content;
|
||||
if (typeof initialContent === 'string') {
|
||||
try {
|
||||
initialContent = JSON.parse(initialContent);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse revision.content:', e);
|
||||
initialContent = [];
|
||||
}
|
||||
}
|
||||
|
||||
const groups = blocks.groups;
|
||||
const draft = ref(initialContent);
|
||||
const isDirty = ref(false);
|
||||
const isDrawerOpen = ref(false);
|
||||
const page = usePage();
|
||||
const autoSaveFunction = ref(true);
|
||||
|
||||
const autosaveInterval = 30000;
|
||||
|
||||
onMounted(() => {
|
||||
initFlowbite();
|
||||
|
||||
// Init auto save
|
||||
setInterval(() => {
|
||||
if (isDirty.value && !isDrawerOpen.value) {
|
||||
autoSaveRevision();
|
||||
}
|
||||
}, autosaveInterval);
|
||||
});
|
||||
|
||||
const {
|
||||
revision,
|
||||
images,
|
||||
revisions
|
||||
} = defineProps({
|
||||
revision: { type: Object, required: true },
|
||||
images: { type: Array, default: () => ([]) },
|
||||
revisions: { type: Object, required: true }
|
||||
});
|
||||
|
||||
// Auto-save function
|
||||
|
||||
const autoSaveRevision = () => {
|
||||
if (autoSaveFunction.value) {
|
||||
isDirty.value = false;
|
||||
|
||||
const payload = {
|
||||
revision_uuid: revision.uuid,
|
||||
title: pageForm.title,
|
||||
content: JSON.stringify(draft.value),
|
||||
publish: pageForm.publish,
|
||||
mainpage: pageForm.mainpage,
|
||||
linked: pageForm.linked,
|
||||
linkorder: pageForm.linkorder,
|
||||
visibility: pageForm.visibility,
|
||||
autosave: true
|
||||
};
|
||||
|
||||
router.patch(route('page-builder.landing-page.patch'), payload, {
|
||||
preserveScroll: true,
|
||||
preserveState: false,
|
||||
onSuccess: () => {
|
||||
showToast('info', 'Endringer lagret automatisk.');
|
||||
},
|
||||
onError: () => {
|
||||
showToast('danger', 'Automatisk lagring misslykkes ...');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cloneBlock = (block) => {
|
||||
block.uuid = uuidv4();
|
||||
return block;
|
||||
};
|
||||
|
||||
const deleteBlock = (index) => {
|
||||
draft.value.splice(index, 1);
|
||||
};
|
||||
|
||||
function handleDrop(event) {
|
||||
const data = event.dataTransfer.getData('application/json');
|
||||
if (data) {
|
||||
try {
|
||||
const block = JSON.parse(data);
|
||||
block.uuid = uuidv4();
|
||||
draft.value.push(block);
|
||||
console.log('📦 Dropped block:', block);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse dropped block", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Drag and drop
|
||||
|
||||
const draggingIndex = ref(null)
|
||||
|
||||
function handleDragStart(event, index) {
|
||||
draggingIndex.value = index;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', index);
|
||||
};
|
||||
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
function handleDropReorder(event, dropIndex) {
|
||||
event.preventDefault();
|
||||
const fromIndex = draggingIndex.value;
|
||||
if (fromIndex === null || fromIndex === dropIndex) return;
|
||||
|
||||
const block = draft.value.splice(fromIndex, 1)[0];
|
||||
draft.value.splice(dropIndex, 0, block);
|
||||
draggingIndex.value = null;
|
||||
};
|
||||
|
||||
// Save form
|
||||
const pageForm = useForm({
|
||||
revision_uuid: revision.uuid,
|
||||
title: revision.title || '',
|
||||
content: '',
|
||||
publish: revision.page.is_published,
|
||||
mainpage: revision.page.main,
|
||||
linked: revision.page.linked,
|
||||
linkorder: revision.page.linkorder,
|
||||
visibility: revision.page.visibility || 'public'
|
||||
});
|
||||
|
||||
const savePage = () => {
|
||||
isDirty.value = false;
|
||||
const dropdown = document.getElementById('dropdownInformation');
|
||||
if (dropdown) dropdown.classList.add('hidden');
|
||||
|
||||
pageForm.content = JSON.stringify(draft.value);
|
||||
pageForm.patch(route('page-builder.landing-page.patch'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
// onSuccess, the user is redirected to the index page for the time being, so toast handling has to be implemented there
|
||||
// Edit: Toast handling is now handled in the PageBuilder layout through it's own component
|
||||
},
|
||||
onError: () => {
|
||||
console.warn("Valideringsfeil ved lagring.")
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* formatDate
|
||||
*
|
||||
* Formats a date string to a Norwegian date format.
|
||||
*
|
||||
* @param {
|
||||
* } dateString
|
||||
*/
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const day = ('0' + date.getDate()).slice(-2);
|
||||
const month = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* dateString
|
||||
*
|
||||
* Formats a date string to a Norwegian time format.
|
||||
* @param {*} dateString
|
||||
*/
|
||||
|
||||
const formatTime = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString('nb-NO', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
// Watch the document for any changes to prevent autosaving if no changes are made
|
||||
watch([draft, pageForm], () => {
|
||||
if (autoSaveFunction.value) {
|
||||
isDirty.value = true;
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// Save as routines and modal handling
|
||||
|
||||
const saveAsModal = ref(false);
|
||||
const saveAsTitle = ref(null);
|
||||
|
||||
const saveAsForm = useForm({
|
||||
page_id: revision.page.id,
|
||||
title: revision.title || '',
|
||||
content: '',
|
||||
publish: revision.page.is_published,
|
||||
mainpage: revision.page.main,
|
||||
linked: revision.page.linked,
|
||||
linkorder: revision.page.linkorder,
|
||||
visibility: revision.page.visibility || 'public'
|
||||
});
|
||||
|
||||
const opensaveAsModal = () => {
|
||||
const dropdown = document.getElementById('dropdownInformation');
|
||||
if (dropdown) dropdown.classList.add('hidden');
|
||||
saveAsModal.value = true;
|
||||
isDrawerOpen.value = true; // Don't autosave while modal is open
|
||||
|
||||
nextTick(() => { // Make sure page is rendered, code inside the tick selects the text in the input field.
|
||||
if (saveAsTitle.value) {
|
||||
const inputElement = saveAsTitle.value.$el;
|
||||
inputElement.focus();
|
||||
inputElement.select();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closesaveAsModal = () => {
|
||||
saveAsForm.reset();
|
||||
saveAsModal.value = false;
|
||||
isDrawerOpen.value = false; // Re-enable auto save
|
||||
};
|
||||
|
||||
const revisionSaveAs = () => {
|
||||
const dropdown = document.getElementById('dropdownInformation');
|
||||
if (dropdown) dropdown.classList.add('hidden');
|
||||
|
||||
saveAsForm.content = JSON.stringify(draft.value);
|
||||
saveAsForm.post(route('page-builder.landing-page.save-as'), {
|
||||
preserveScroll: false,
|
||||
onSuccess: () => closesaveAsModal(),
|
||||
});
|
||||
};
|
||||
|
||||
// Open revision routines and modal handling
|
||||
|
||||
const openModal = ref(false);
|
||||
const rowClicked = ref(false);
|
||||
|
||||
const openForm = useForm({
|
||||
revision_uuid: ''
|
||||
});
|
||||
|
||||
const showOpenModal = () => {
|
||||
const dropdown = document.getElementById('dropdownInformation');
|
||||
if (dropdown) dropdown.classList.add('hidden');
|
||||
|
||||
openModal.value = true;
|
||||
isDrawerOpen.value = true; // Don't autosave while modal is open
|
||||
};
|
||||
|
||||
const closeOpenModal = () => {
|
||||
openModal.value = false;
|
||||
isDrawerOpen.value = false; // Re-enable auto save
|
||||
|
||||
openForm.reset();
|
||||
rowClicked.value = false;
|
||||
};
|
||||
|
||||
const selectRevision = (uuid, index) => {
|
||||
openForm.revision_uuid = uuid;
|
||||
|
||||
if (uuid != revision.uuid) { // Prevent opening of current revision
|
||||
rowClicked.value = index;
|
||||
}
|
||||
};
|
||||
|
||||
const openRevision = () => {
|
||||
router.get(route('page-builder.builder.edit', openForm.revision_uuid));
|
||||
};
|
||||
|
||||
// Set active routines
|
||||
|
||||
const setActiveForm = useForm({
|
||||
revision_uuid: revision.uuid,
|
||||
title: revision.title || '',
|
||||
content: '',
|
||||
});
|
||||
|
||||
const setActive = () => {
|
||||
const dropdown = document.getElementById('dropdownInformation');
|
||||
if (dropdown) dropdown.classList.add('hidden');
|
||||
|
||||
setActiveForm.content = JSON.stringify(draft.value);
|
||||
setActiveForm.put(route('page-builder.landing-page.set-active'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show="openModal" @close="closeOpenModal()" max-width="4xl">
|
||||
<div class="p-6 bg-white dark:bg-gray-800 rounded-md">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-400">
|
||||
Tilgjengelige revisjoner
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-200">
|
||||
Velg en versjon av denne siden du ønsker å åpne
|
||||
</p>
|
||||
|
||||
<div class="mt-2 relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Tittel
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Forfatter
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Versjon
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Oppdatert
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Opprettet
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(pageRevision, index) in revisions"
|
||||
:key="index"
|
||||
class="dark:border-gray-700 border-gray-200"
|
||||
:class="{
|
||||
'bg-gray-100 dark:bg-gray-600': rowClicked == index + 1,
|
||||
'bg-white dark:bg-gray-800': !rowClicked,
|
||||
'cursor-pointer': pageRevision.uuid != revision.uuid
|
||||
}"
|
||||
@click="selectRevision(pageRevision.uuid, index + 1)"
|
||||
>
|
||||
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<div class="flex gap-2 justify-normal items-center">
|
||||
<span>
|
||||
{{ pageRevision.title }}
|
||||
</span>
|
||||
<span v-show="pageRevision.active" class="w-4 h-4 text-yellow-600 dark: dark:text-yellow-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<td class="px-6 py-4">
|
||||
{{ pageRevision.author.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ pageRevision.version }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ formatDate(pageRevision.updated_at) }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ formatDate(pageRevision.created_at) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<SecondaryButton @click="closeOpenModal()"> Avbryt </SecondaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': openForm.processing }"
|
||||
:disabled="openForm.processing"
|
||||
v-show="rowClicked"
|
||||
@click="openRevision"
|
||||
>
|
||||
Åpne
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal :show="saveAsModal" @close="closesaveAsModal()">
|
||||
<div class="p-6 bg-white dark:bg-gray-800 rounded-md">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-400">
|
||||
Lagre som ny revisjon
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-200">
|
||||
Dette vil opprette en ny versjon og kopi av siden
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<InputLabel for="titleSaveAs" value="Ny tittel" class="text-black dark:text-gray-400" />
|
||||
|
||||
<TextInput
|
||||
id="titleSaveAs"
|
||||
ref="saveAsTitle"
|
||||
type="text"
|
||||
class="mt-1 block w-full dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
placeholder="Oppgi ny tittel"
|
||||
v-model="saveAsForm.title"
|
||||
/>
|
||||
|
||||
<InputError :message="saveAsForm.errors.title" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<SecondaryButton @click="closesaveAsModal()"> Avbryt </SecondaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': saveAsForm.processing }"
|
||||
:disabled="saveAsForm.processing"
|
||||
@click="revisionSaveAs"
|
||||
>
|
||||
Lagre som
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Head title="Rediger Side" />
|
||||
|
||||
<PageBuilderLayout>
|
||||
<template #header>
|
||||
<AppLogo />
|
||||
<h3 class="text-lg font-semibold">Rediger "{{ revision.title }}"</h3>
|
||||
<div>
|
||||
|
||||
<button id="dropdownInformationButton" data-dropdown-toggle="dropdownInformation" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
|
||||
Handlinger
|
||||
<svg class="w-2.5 h-2.5 ms-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div id="dropdownInformation" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
|
||||
<div class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
||||
<div>{{ page.props.auth.user.name }}</div>
|
||||
<div class="font-medium truncate">{{ page.props.auth.user.email }}</div>
|
||||
</div>
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownInformationButton">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click="showOpenModal"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>
|
||||
Åpne
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="savePage"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>
|
||||
Lagre
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click="opensaveAsModal"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>
|
||||
Lagre som
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
:href="route('page-builder.landing-page.preview', revision.uuid)"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>
|
||||
Forhåndsvis
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="!revision.active" class="py-2">
|
||||
<a href="#" @click.prevent="setActive" class="block px-4 py-2 btn btn-sm btn-ghost hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
Sett aktiv
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<Link :href="route('page-builder.index')" class="block px-4 py-2 btn btn-sm btn-ghost hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
Lukk
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- Sidebar -->
|
||||
<div class="bg-slate-100 overflow-y-auto w-1/6 p-4 space-y-4">
|
||||
<div class="space-y-4 pt-4 pb-2 pl-4 pr-4 rounded border dark:border-gray-600 bg-gray-50 dark:bg-gray-800">
|
||||
<div>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="autoSaveFunction" class="sr-only peer">
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
Autolagring <span v-if="autoSaveFunction">På</span><span v-else>Av</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="revision.active" class="space-y-4 p-4 rounded border dark:border-gray-600 bg-gray-50 dark:bg-gray-800">
|
||||
<div class="mb-4">
|
||||
<label for="page_title-text" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
Sidetittel
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="page_title-text"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
placeholder="Skriv inn en tittel"
|
||||
v-model="pageForm.title"
|
||||
>
|
||||
<InputError :message="pageForm.errors.title" class="mt-2" />
|
||||
<p id="page_title-text-explanation" class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Tittelen på siden vil være synlig i fanearket i nettleseren hos besøkende, samt brukes til å generere URL (slug)
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="pageForm.linked" class="sr-only peer">
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Menylenke</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="steps-range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Menylenke rekkefølge</label>
|
||||
<input id="steps-range" :disabled="!pageForm.linked" v-model="pageForm.linkorder" type="range" min="0" max="20" step="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700">
|
||||
<span class="text-xs">Prioritet: {{ pageForm.linkorder }} (høyere verdi kommer først)</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="pageForm.publish" class="sr-only peer">
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Publisert</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="pageForm.mainpage" class="sr-only peer">
|
||||
<div class="relative w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
|
||||
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Sett som hovedside</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center mb-4">
|
||||
<input id="page_state-radio-1" v-model="pageForm.visibility" type="radio" value="public" name="page_state-radio"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="page_state-radio-1" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Synlig for alle</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input id="page_state-radio-2" v-model="pageForm.visibility" type="radio" value="private" name="page_state-radio"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="page_state-radio-2" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Bare synlig for medlemmer</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block groups -->
|
||||
<BlockGroup
|
||||
v-for="group in groups"
|
||||
:key="group.uuid"
|
||||
:title="group.title"
|
||||
:blocks="group.blocks"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Canvas -->
|
||||
<div class="bg-white dark:bg-gray-800 w-5/6 p-6 overflow-y-auto" @dragover.prevent @drop="handleDrop">
|
||||
<div v-if="draft.length === 0" class="text-gray-500 text-sm italic">
|
||||
Dra inn blokker fra venstre for å begynne.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(block, index) in draft"
|
||||
:key="block.uuid"
|
||||
class="mb-4"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, index)"
|
||||
@dragover="handleDragOver"
|
||||
@drop="handleDropReorder($event, index)"
|
||||
>
|
||||
<BlockWrapper
|
||||
:images="images"
|
||||
:block="block"
|
||||
@delete="() => deleteBlock(index)"
|
||||
@drawer-state="isDrawerOpen = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageBuilderLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* scoped styles for the edit screen */
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/layouts/AuthenticatedLayout.vue';
|
||||
import Modal from '@/Components/Modal.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import DangerButton from '@/Components/DangerButton.vue';
|
||||
|
||||
import { Head, usePage, useForm, Link, router } from '@inertiajs/vue3';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
|
||||
onMounted(() => {
|
||||
initFlowbite();
|
||||
});
|
||||
|
||||
const page = usePage();
|
||||
const userRoles = computed(() => page.props.userRoles);
|
||||
const userPermissions = computed(() => page.props.userPermissions);
|
||||
|
||||
const {
|
||||
builtPages,
|
||||
} = defineProps({
|
||||
builtPages: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// Check user permissions from Laravel
|
||||
|
||||
const hasPermission = (permissionName) => {
|
||||
return userPermissions.value.some(userPermissions => userPermissions.name === permissionName);
|
||||
};
|
||||
|
||||
// Get thumbnail picture from the content property
|
||||
|
||||
const getThumbs = (lPage) => {
|
||||
try {
|
||||
const blocks = typeof lPage.content === 'string'
|
||||
? JSON.parse(lPage.content)
|
||||
: lPage.content;
|
||||
|
||||
return blocks
|
||||
.filter(block => block.thumbUrl)
|
||||
.map(block => block.thumbUrl);
|
||||
} catch (e) {
|
||||
console.warn('Invalid JSON in page content:', e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Confirm deletion modal
|
||||
|
||||
const deleteModal = ref(false);
|
||||
|
||||
const deleteForm = useForm({
|
||||
id: null
|
||||
});
|
||||
|
||||
const openDeleteModal = (lPage) => {
|
||||
deleteForm.id = lPage.id;
|
||||
deleteModal.value = true;
|
||||
};
|
||||
|
||||
const closeDeleteModal = () => {
|
||||
deleteForm.reset();
|
||||
deleteModal.value = false;
|
||||
};
|
||||
|
||||
const deletePage = () => {
|
||||
deleteForm.delete(route('page-builder.landing-page.delete'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeDeleteModal(),
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show="deleteModal" @close="closeDeleteModal()">
|
||||
<div class="p-6 bg-white dark:bg-gray-800 rounded-md">
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-400">
|
||||
Bekreft sletting av side
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-200">
|
||||
Denne operasjonen er permanent og kan ikke angres. All data
|
||||
knyttet til siden vil bli slettet. Hvis siden er satt som
|
||||
hovedsiden, vil alle besøkende bli redirigert til brukersiden
|
||||
(hvis brukeren er innlogget), eller innloggingsiden hvis
|
||||
den besøkende ikke er autentisert, inntill du setter en ny
|
||||
forside.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<SecondaryButton @click="closeDeleteModal()"> Avbryt </SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ml-3"
|
||||
:class="{ 'opacity-25': deleteForm.processing }"
|
||||
:disabled="deleteForm.processing"
|
||||
@click="deletePage"
|
||||
>
|
||||
Slett side
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Head title="Sidebygger" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight dark:text-gray-400">Administrering av sider</h2>
|
||||
<h4 class="text-sm mt-2 text-gray-800 leading-tight dark:text-gray-400">Her kan du opprette en ny, eller administrere eksisterende sider</h4>
|
||||
</div>
|
||||
<Link :href="route('page-builder.landing-page.create')" v-if="hasPermission('create-article')" class="relative inline-flex items-center justify-center p-0.5 mb-2 mr-2 overflow-hidden text-sm font-medium text-gray-900 rounded-lg group bg-gradient-to-br from-cyan-500 to-blue-500 group-hover:from-cyan-500 group-hover:to-blue-500 hover:text-white dark:text-white focus:ring-4 focus:outline-none focus:ring-cyan-200 dark:focus:ring-cyan-800">
|
||||
<span class="flex items-center relative px-5 py-2.5 transition-all ease-in duration-75 bg-white dark:bg-gray-700 rounded-md group-hover:bg-opacity-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 -960 960 960" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-2">
|
||||
<path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h440l200 200v440q0 33-23.5 56.5T760-120H200Zm0-80h560v-400H600v-160H200v560Zm80-80h400v-80H280v80Zm0-320h200v-80H280v80Zm0 160h400v-80H280v80Zm-80-320v160-160 560-560Z" />
|
||||
</svg>
|
||||
Ny side
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-4">
|
||||
<div
|
||||
v-for="page in builtPages"
|
||||
:key="page.id"
|
||||
class="flex flex-col items-center bg-white border border-gray-200 rounded-lg shadow-sm md:flex-row md:max-w-xl dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex flex-col md:w-48 w-full rounded-t-lg md:rounded-none md:rounded-s-lg overflow-hidden">
|
||||
<img
|
||||
v-for="(thumb, index) in getThumbs(page)"
|
||||
:key="index"
|
||||
:src="thumb"
|
||||
alt="Forhåndsvisning"
|
||||
class="object-cover w-full h-24 md:h-24 border-b dark:border-gray-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between p-4 leading-normal h-full w-full">
|
||||
<div>
|
||||
<h3 class="mb-2 text-xl font-semibold tracking-tight text-gray-900 dark:text-white">{{ page.title }}</h3>
|
||||
<p class="mb-3 font-normal text-gray-700 dark:text-gray-400">Av: {{ page.author.name }} — Revisjoner: {{ page.revisions_count }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<Link
|
||||
v-if="page.activeRevision"
|
||||
:href="route('page-builder.builder.edit', page.activeRevision.uuid)"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
>
|
||||
Rediger
|
||||
</Link>
|
||||
<button
|
||||
@click="openDeleteModal(page)"
|
||||
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900"
|
||||
>
|
||||
Slett
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,296 @@
|
||||
<script setup>
|
||||
import { Head, Link, router, usePage} from '@inertiajs/vue3';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
|
||||
import { on } from '@/Components/EventBus.vue';
|
||||
|
||||
import MainMenu from '@/Components/FrontPage/MenyComponent.vue';
|
||||
import ExternalMainMenu from '@/Components/FrontPage/ExternalMenuComponent.vue';
|
||||
import SmallHeader from '@/Components/FrontPage/SmallHeader.vue';
|
||||
|
||||
import HeroLogoTextRendered from '@/Components/Blocks/Heroes/HeroLogoTextRendered.vue';
|
||||
|
||||
import FAQListRendered from '@/Components/Blocks/FAQ/FAQListRendered.vue';
|
||||
import FAQCollapseRendered from '@/Components/Blocks/FAQ/FAQCollapseRendered.vue';
|
||||
import FAQCondensedRendered from '@/Components/Blocks/FAQ/FAQCondensedRendered.vue';
|
||||
|
||||
import TwoColumnTextRendered from '@/Components/Blocks/Text/TwoColumnTextRendered.vue';
|
||||
|
||||
import FrontPageMenuHeaderRendered from '@/Components/Blocks/Statics/FrontPageMenuHeaderRendered.vue';
|
||||
import FrontPageSponsorsRendered from '@/Components/Blocks/Statics/FrontPageSponsorsRendered.vue';
|
||||
import FrontPageMembershipInfoRendered from '@/Components/Blocks/Statics/FrontPageMembershipInfoRendered.vue';
|
||||
import FrontPageNewsListRendered from '@/Components/Blocks/Statics/FrontPageNewsListRendered.vue';
|
||||
import FrontPageEventsListRendered from '@/Components/Blocks/Statics/FrontPageEventsListRendered.vue';
|
||||
import PageFooterRendered from '@/Components/Blocks/Statics/PageFooterRendered.vue';
|
||||
|
||||
const {
|
||||
revision,
|
||||
images,
|
||||
blockData,
|
||||
menuLinks
|
||||
} = defineProps({
|
||||
revision: { type: Object, required: true },
|
||||
images: { type: Array, default: () => ([]) },
|
||||
blockData: { type: Object, default: () => ({}) },
|
||||
menuLinks: { type: Object, default: () => ({}) }
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// Check if the url has the parameter key scrollTo
|
||||
if (urlParam('scrollTo')) {
|
||||
// Enable scrolling on the body and html elements
|
||||
document.body.style.overflow = 'auto';
|
||||
document.documentElement.style.overflow = 'auto';
|
||||
// Get the value of the parameter
|
||||
const scrollTo = urlParam('scrollTo');
|
||||
|
||||
// Scroll to the section with the id that matches the value of the parameter
|
||||
gsap.to(window, {
|
||||
duration: 1,
|
||||
scrollTo: {
|
||||
y: `#${scrollTo}`,
|
||||
autoKill: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
gsap.registerPlugin(ScrollToPlugin, ScrollTrigger);
|
||||
|
||||
const useEventBus = ref(false);
|
||||
const showMainMenu = ref(false);
|
||||
const pageRef = ref();
|
||||
const page = usePage();
|
||||
const smallHeader = ref(null);
|
||||
|
||||
const showErrorModal = ref(false);
|
||||
const errorStatus = ref('');
|
||||
|
||||
const closeErrorModal = () => {
|
||||
showErrorModal.value = false;
|
||||
};
|
||||
|
||||
// Lytt etter errorModal event fra Laravel
|
||||
window.addEventListener('errorModal', (event) => {
|
||||
showErrorModal.value = true;
|
||||
errorStatus.value = event.detail.status;
|
||||
});
|
||||
|
||||
/** Menu button animations */
|
||||
const menuButton = ref(null);
|
||||
|
||||
const handleMenuButtonOver = () => {
|
||||
gsap.set(menuButton.value, {
|
||||
borderRadius: '0.375rem',
|
||||
});
|
||||
gsap.to(menuButton.value, {
|
||||
duration: 0.2,
|
||||
scale: 1.1,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMenuButtonOut = () => {
|
||||
gsap.set(menuButton.value, {
|
||||
borderRadius: '9999px',
|
||||
});
|
||||
gsap.to(menuButton.value, {
|
||||
duration: 0.2,
|
||||
scale: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const clickMenuButton = () => {
|
||||
showMainMenu.value = !showMainMenu.value;
|
||||
|
||||
if (showMainMenu.value) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
document.documentElement.style.overflow = 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
/** Get a node value from json */
|
||||
|
||||
const extractMetaValue = (node, key) => {
|
||||
let meta = JSON.parse(node.fs_meta);
|
||||
|
||||
return meta[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* formatDate
|
||||
*
|
||||
* Formats a date string to a Norwegian date format.
|
||||
*
|
||||
* @param {
|
||||
* } dateString
|
||||
*/
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const day = ('0' + date.getDate()).slice(-2);
|
||||
const month = ('0' + (date.getMonth() + 1)).slice(-2);
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* dateString
|
||||
*
|
||||
* Formats a date string to a Norwegian time format.
|
||||
* @param {*} dateString
|
||||
*/
|
||||
|
||||
const formatTime = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleTimeString('nb-NO', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
/** Get url parameters */
|
||||
|
||||
const urlParam = (paramKey) => {
|
||||
const url = new URL(window.location.href);
|
||||
return url.searchParams.get(paramKey);
|
||||
};
|
||||
|
||||
// Denne funksjonen kalles fra lenker på siden for å
|
||||
// Gi siden en myk fade ut effekt før den nye siden lastes
|
||||
const loadLink = (url) => {
|
||||
gsap.to(pageRef.value, {
|
||||
duration: 1,
|
||||
opacity: 0,
|
||||
ease: 'power2.inOut',
|
||||
onComplete: () => {
|
||||
router.visit(url);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const components = {
|
||||
HeroLogoTextRendered,
|
||||
|
||||
FAQListRendered,
|
||||
FAQCollapseRendered,
|
||||
FAQCondensedRendered,
|
||||
|
||||
TwoColumnTextRendered,
|
||||
|
||||
FrontPageMenuHeaderRendered,
|
||||
FrontPageSponsorsRendered,
|
||||
FrontPageMembershipInfoRendered,
|
||||
FrontPageNewsListRendered,
|
||||
FrontPageEventsListRendered,
|
||||
PageFooterRendered
|
||||
};
|
||||
|
||||
/** Eventbus listeners */
|
||||
|
||||
on('linkClicked', (payload) => {
|
||||
if (payload === 'om' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
gsap.to(window, {
|
||||
duration: 1,
|
||||
scrollTo: {
|
||||
y: '#content',
|
||||
autoKill: false,
|
||||
},
|
||||
});
|
||||
} else if (payload === 'arrangementer' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
gsap.to(window, {
|
||||
duration: 1,
|
||||
scrollTo: {
|
||||
y: '#events',
|
||||
autoKill: false,
|
||||
},
|
||||
});
|
||||
} else if (payload === 'nyheter' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
gsap.to(window, {
|
||||
duration: 1,
|
||||
scrollTo: {
|
||||
y: '#intro',
|
||||
autoKill: false,
|
||||
},
|
||||
});
|
||||
} else if (payload === 'sponsor' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
gsap.to(window, {
|
||||
duration: 1,
|
||||
scrollTo: {
|
||||
y: '#sponsors-intro',
|
||||
autoKill: false,
|
||||
},
|
||||
});
|
||||
} else if (payload === 'medlem' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
gsap.to(window, {
|
||||
duration: 1,
|
||||
scrollTo: {
|
||||
y: '#membership',
|
||||
autoKill: false,
|
||||
},
|
||||
});
|
||||
} else if (payload === 'medlemside' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
loadLink(route('dashboard'));
|
||||
} else if (payload === 'loggpaa' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
loadLink(route('login'));
|
||||
} else if (payload === 'registrer' && useEventBus.value) {
|
||||
clickMenuButton();
|
||||
loadLink(route('register'));
|
||||
}
|
||||
});
|
||||
|
||||
on('eventBusActive', (payload) => {
|
||||
useEventBus.value = payload;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<title>{{ revision.title }}</title>
|
||||
</Head>
|
||||
|
||||
<div ref="pageRef" class="min-h-screen w-full bg-black">
|
||||
<template v-if="revision.page.main"> <!-- Show Main menu if the current page is the front page (main page) -->
|
||||
<MainMenu v-if="showMainMenu" :active="showMainMenu" :pageSettings="page.props.pageSettings" :menuLinks="menuLinks" />
|
||||
</template>
|
||||
<template v-else> <!-- Show the external main menu, if the current page is NOT the front page (main page) -->
|
||||
<ExternalMainMenu v-if="showMainMenu" :active="showMainMenu" :pageSettings="page.props.pageSettings" class="z-40" />
|
||||
</template>
|
||||
<SmallHeader />
|
||||
|
||||
<!-- Hovedmeny knapp -->
|
||||
<div class="fixed top-0 right-0 p-4 z-50">
|
||||
<button ref="menuButton" @click="clickMenuButton" @mouseover="handleMenuButtonOver" @mouseout="handleMenuButtonOut" class="p-2 text-white rounded-full shadow-md shadow-black" :class="{'bg-sky-600': showMainMenu, 'bg-green-700': !showMainMenu }">
|
||||
<svg v-if="showMainMenu" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Komponentrendring -->
|
||||
<component
|
||||
v-for="block in revision.content"
|
||||
:key="block.uuid"
|
||||
:images="images"
|
||||
:is="components[block.renderComponentName]"
|
||||
:block="block"
|
||||
:blockData="blockData"
|
||||
:frontpage="revision.page.main"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
Reference in New Issue
Block a user