parent
20e1eac2f1
commit
fbe7d4cf3d
19 changed files with 526 additions and 282 deletions
@ -0,0 +1,31 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration; |
||||||
|
use Illuminate\Database\Schema\Blueprint; |
||||||
|
use Illuminate\Support\Facades\DB; |
||||||
|
use Illuminate\Support\Facades\Schema; |
||||||
|
|
||||||
|
return new class extends Migration |
||||||
|
{ |
||||||
|
/** |
||||||
|
* Run the migrations. |
||||||
|
*/ |
||||||
|
public function up(): void |
||||||
|
{ |
||||||
|
$aps = DB::table("album_photo")->get(); |
||||||
|
foreach($aps as $ap) { |
||||||
|
$photo = DB::table("photos")->where("id", $ap->photo_id)->first(); |
||||||
|
DB::table("album_photo")->where("id", $ap->id)->update([ |
||||||
|
"created_at" => $photo->created_at |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reverse the migrations. |
||||||
|
*/ |
||||||
|
public function down(): void |
||||||
|
{ |
||||||
|
// |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,68 @@ |
|||||||
|
<script setup> |
||||||
|
import { reactive } from 'vue'; |
||||||
|
import PanelCreate from './PanelCreate.vue'; |
||||||
|
import PanelDisplay from './PanelDisplay.vue'; |
||||||
|
import axios from 'axios'; |
||||||
|
|
||||||
|
const props = defineProps({ |
||||||
|
album: { |
||||||
|
type: Object, |
||||||
|
required: true, |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const uuids = async () => { |
||||||
|
let res = await axios.get(route("album.photo.uuid", props.album.uuid)); |
||||||
|
console.log(res.data.uuids); |
||||||
|
panelState.uuids = res.data.uuids; |
||||||
|
} |
||||||
|
|
||||||
|
const panelState = reactive({ toggle: true, uuids: [] }); |
||||||
|
|
||||||
|
const imageState = reactive({ |
||||||
|
"url": "", |
||||||
|
}) |
||||||
|
|
||||||
|
const emits = defineEmits(["close", "data"]); |
||||||
|
|
||||||
|
const imageAdded = (file) => { |
||||||
|
imageState.url = file.url; |
||||||
|
} |
||||||
|
|
||||||
|
const imageRemoved = (file) => { |
||||||
|
imageState.url = ""; |
||||||
|
} |
||||||
|
|
||||||
|
uuids(); |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="text-left text-black shadow-2xl rounded-lg bg-gray-50 |
||||||
|
border border-gray-300" :class="{ 'w-[30rem]': !panelState.toggle, 'w-[60rem] h-[40rem]': panelState.toggle, }"> |
||||||
|
<div v-if="imageState.url" class="max-h-72 overflow-hidden flex items-center"> |
||||||
|
<img :src="imageState.url"> |
||||||
|
</div> |
||||||
|
<div class="px-10 pb-5 pt-5"> |
||||||
|
<div class="w-full flex justify-between mb-5 items-center"> |
||||||
|
<select @change="panelState.toggle = !panelState.toggle" class="bg-gray-200 border-none rounded shadow"> |
||||||
|
<option>Photo Existante</option> |
||||||
|
<option>Nouvelle photo</option> |
||||||
|
</select> |
||||||
|
<img @click="() => emits('close')" src="/icons/cancel.svg" class="h-7 hover:bg-black/10 rounded-md p-1"> |
||||||
|
</div> |
||||||
|
<PanelDisplay |
||||||
|
v-show="panelState.toggle" |
||||||
|
@data="(uuids) => emits('data', uuids)" |
||||||
|
:hidden-photos="panelState.uuids" |
||||||
|
/> |
||||||
|
<PanelCreate |
||||||
|
v-show="!panelState.toggle" |
||||||
|
@file-added="imageAdded" |
||||||
|
@file-removed="imageRemoved" |
||||||
|
@data="(uuid) => emits('data', [uuid])" |
||||||
|
@close="emits('close')" |
||||||
|
:redirect="false" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,86 @@ |
|||||||
|
<script setup> |
||||||
|
import { useForm } from '@inertiajs/vue3'; |
||||||
|
import File from '@/file'; |
||||||
|
import Layout from '@/Layouts/Layout.vue'; |
||||||
|
import Dropzone from '@/Components/Dropzone.vue'; |
||||||
|
import TextInput from '@/Components/TextInput.vue'; |
||||||
|
import InputError from '@/Components/InputError.vue'; |
||||||
|
import { ref } from 'vue'; |
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({ |
||||||
|
redirect: { |
||||||
|
type: Boolean, |
||||||
|
default: true, |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const dropzone = ref(null); |
||||||
|
|
||||||
|
const form = useForm({ |
||||||
|
name: "", |
||||||
|
path: "", |
||||||
|
redirect: props.redirect, |
||||||
|
}) |
||||||
|
|
||||||
|
const emits = defineEmits(["data", "close", "file-added", "file-removed"]); |
||||||
|
|
||||||
|
const imageAdded = (file) => { |
||||||
|
form.path = file.key; |
||||||
|
if(form.name === "") form.name = File.Basename(file.key).split("_")[1]; |
||||||
|
emits('file-added', file) |
||||||
|
} |
||||||
|
|
||||||
|
const imageRemoved = (file) => { |
||||||
|
form.path = ""; |
||||||
|
emits('file-removed', file) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const submit = async () => { |
||||||
|
if(!props.redirect) { |
||||||
|
let res = await axios.post("/photo", { |
||||||
|
name: form.name, |
||||||
|
path: form.path, |
||||||
|
redirect: false, |
||||||
|
}) |
||||||
|
const uuid = res.data.uuid; |
||||||
|
console.log(uuid, res); |
||||||
|
emits("data", uuid); |
||||||
|
} else { |
||||||
|
form.post("/photo", { |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/json", |
||||||
|
"X-CSRF-Token": document.querySelector('input[name=_token]').value, |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
form.path = ""; |
||||||
|
form.name = ""; |
||||||
|
dropzone.value.removeFiles(); |
||||||
|
emits("close"); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<form @submit.prevent="submit"> |
||||||
|
<p>Photo</p> |
||||||
|
<Dropzone |
||||||
|
ref="dropzone" |
||||||
|
@file-added="imageAdded" |
||||||
|
@file-removed="imageRemoved" |
||||||
|
:name="photos" |
||||||
|
:empty="'Téléversé une image'" |
||||||
|
:multiple="false" |
||||||
|
:accept="'image/*'" |
||||||
|
:class="'mb-1'" |
||||||
|
/> |
||||||
|
<InputError :message="form.errors.path"/> |
||||||
|
<p class="mt-3">Nom</p> |
||||||
|
<TextInput :class="'mb-1 w-full'" v-model="form.name" required :placeholder="'Nom de la photo'"/> |
||||||
|
<InputError :message="form.errors.name"/> |
||||||
|
<div class="w-full flex justify-end mt-3"> |
||||||
|
<button type="submit" class="text-white font-semibold p-1 px-2 bg-primary rounded-md">Ajouter</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</template> |
@ -0,0 +1,78 @@ |
|||||||
|
<script setup> |
||||||
|
import axios from 'axios'; |
||||||
|
import { reactive } from 'vue'; |
||||||
|
import Show from './Show.vue'; |
||||||
|
import PanelShow from './PanelShow.vue'; |
||||||
|
const props = defineProps({ |
||||||
|
hiddenPhotos: { |
||||||
|
type: Array, |
||||||
|
default: [] |
||||||
|
} |
||||||
|
}) |
||||||
|
const emits = defineEmits(["data"]); |
||||||
|
|
||||||
|
const albumState = reactive({albums: [], focusAlbum: "", photos: [], pageCount: 1, lastPage: 1}); |
||||||
|
|
||||||
|
let photos = []; |
||||||
|
|
||||||
|
|
||||||
|
const loadPhotos = async (album, page) => { |
||||||
|
if(!album || album === "") { |
||||||
|
let res = await axios.get(route("photo.page", page)); |
||||||
|
albumState.photos = albumState.photos.concat(res.data.photos); |
||||||
|
albumState.lastPage = res.data.lastPage; |
||||||
|
} else { |
||||||
|
let res = await axios.get("/album/" + album + "/page/" + page); |
||||||
|
albumState.photos = albumState.photos.concat(res.data.photos); |
||||||
|
albumState.lastPage = res.data.lastPage; |
||||||
|
} |
||||||
|
} |
||||||
|
const loadAlbum = (album) => { |
||||||
|
albumState.photos = []; |
||||||
|
albumState.focusAlbum = album; |
||||||
|
albumState.pageCount = 1; |
||||||
|
loadPhotos(albumState.focusAlbum, albumState.pageCount); |
||||||
|
} |
||||||
|
|
||||||
|
const loadAlbums = async (page) => { |
||||||
|
let res = await axios.get(route("album.page", page)); |
||||||
|
albumState.albums = res.data.albums; |
||||||
|
} |
||||||
|
const initLoad = async () => { |
||||||
|
await loadAlbums(1); |
||||||
|
loadAlbum(""); |
||||||
|
} |
||||||
|
initLoad(); |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex pb-10 h-[35rem]"> |
||||||
|
<div class="flex flex-col items-start justify-between w-32 border-r border-gray-200 pr-2"> |
||||||
|
<div class="w-full"> |
||||||
|
<p class="my-1 py-1 px-2 text-gray-700 rounded shadow" |
||||||
|
@click="loadAlbum('')" |
||||||
|
:class="{ 'bg-gray-200': albumState.focusAlbum === '', 'bg-gray-100': albumState.focusAlbum !== ''}">Photothèque</p> |
||||||
|
<p v-for="album in albumState.albums" |
||||||
|
@click="loadAlbum(album.uuid)" |
||||||
|
class="my-1 py-1 px-2 bg-gray-100 text-gray-700 |
||||||
|
overflow-hidden text-nowrap rounded shadow" |
||||||
|
:class="{ 'bg-gray-200': albumState.focusAlbum === album.uuid, 'bg-gray-100': albumState.focusAlbum !== album.uuid}">{{ album.name }}</p> |
||||||
|
<p class="my-1 py-1 px-2 bg-gray-100 text-gray-700 rounded shadow">plus d'album</p> |
||||||
|
</div> |
||||||
|
<button @click="emits('data', photos)" class="text-white font-semibold p-1 px-2 bg-primary rounded-md">Ajouter</button> |
||||||
|
</div> |
||||||
|
<div class="w-[calc(100%-8rem)] h-full overflow-y-scroll py-5 ml-2"> |
||||||
|
<div class="grid grid-cols-3"> |
||||||
|
<PanelShow v-for="(photo, index) in albumState.photos" v-show="!props.hiddenPhotos.includes(photo.uuid)" :photo="photo" |
||||||
|
:index="index" :length="albumState.photos.length" :columns="3" :active="photos.includes(photo.uuid)" |
||||||
|
@photo-added="(uuid) => { if(!photos.includes(uuid)) photos.push(uuid) }" |
||||||
|
@photo-removed="(uuid) => { if(photos.includes(uuid)) photos = photos.filter(uu => uu != uuid) }" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<div v-if="albumState.pageCount < albumState.lastPage" class="py-2 w-full flex justify-center"> |
||||||
|
<button @click="loadPhotos(albumState.focusAlbum, ++albumState.pageCount)" class="bg-gray-100 p-2 font-medium text-gray-700 |
||||||
|
rounded shadow hover:scale-[1.01]">Afficher plus de photos</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,63 @@ |
|||||||
|
<script setup> |
||||||
|
import { Link } from '@inertiajs/vue3'; |
||||||
|
import { useForm } from '@inertiajs/vue3'; |
||||||
|
import { reactive } from 'vue'; |
||||||
|
import Edit from './Edit.vue'; |
||||||
|
import axios from 'axios'; |
||||||
|
|
||||||
|
const props = defineProps({ |
||||||
|
photo: { |
||||||
|
type: Object, |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
active: { |
||||||
|
type: Boolean, |
||||||
|
default: false, |
||||||
|
}, |
||||||
|
index: { |
||||||
|
type: Number, |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
length: { |
||||||
|
type: Number, |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
columns: { |
||||||
|
type: Number, |
||||||
|
default: 3, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
const emits = defineEmits([ "photo-added", "photo-removed" ]); |
||||||
|
|
||||||
|
let flag = false; |
||||||
|
|
||||||
|
const imageChange = () => { |
||||||
|
flag = !flag; |
||||||
|
if (flag) emits("photo-added", props.photo.uuid); |
||||||
|
else emits("photo-removed", props.photo.uuid); |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div :class="{ |
||||||
|
'border-r': (props.index + 1) % props.columns != 0, |
||||||
|
'border-b': props.index < props.length - props.columns, |
||||||
|
'h-56': props.columns === 3, |
||||||
|
}" |
||||||
|
class="group relative w-full overflow-hidden border-white hover:scale-[1.003] flex items-center bg-black/90"> |
||||||
|
<div class="absolute left-0 right-0 top-0 p-2 flex justify-between"> |
||||||
|
<div class="flex items-center"> |
||||||
|
<input @click="imageChange" type="checkbox" class="bg-black/50 p-1 rounded-md mr-2" :checked="props.active"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="hidden absolute left-0 right-0 bottom-0 p-2 group-hover:flex justify-between flex-wrap items-end"> |
||||||
|
<div class="text-right bg-black/30 p-1 px-3 rounded-md mt-1"> |
||||||
|
<p class="text-sm text-white">{{ props.photo.name }}</p> |
||||||
|
<p class="text-sm text-white">publier par {{ props.photo.user.name }}</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<img :src="props.photo.path" class="w-full bg-white"> |
||||||
|
</div> |
||||||
|
</template> |
@ -1,108 +0,0 @@ |
|||||||
<script setup> |
|
||||||
import DangerButton from '@/Components/DangerButton.vue'; |
|
||||||
import InputError from '@/Components/InputError.vue'; |
|
||||||
import InputLabel from '@/Components/InputLabel.vue'; |
|
||||||
import Modal from '@/Components/Modal.vue'; |
|
||||||
import SecondaryButton from '@/Components/SecondaryButton.vue'; |
|
||||||
import TextInput from '@/Components/TextInput.vue'; |
|
||||||
import { useForm } from '@inertiajs/vue3'; |
|
||||||
import { nextTick, ref } from 'vue'; |
|
||||||
|
|
||||||
const confirmingUserDeletion = ref(false); |
|
||||||
const passwordInput = ref(null); |
|
||||||
|
|
||||||
const form = useForm({ |
|
||||||
password: '', |
|
||||||
}); |
|
||||||
|
|
||||||
const confirmUserDeletion = () => { |
|
||||||
confirmingUserDeletion.value = true; |
|
||||||
|
|
||||||
nextTick(() => passwordInput.value.focus()); |
|
||||||
}; |
|
||||||
|
|
||||||
const deleteUser = () => { |
|
||||||
form.delete(route('profile.destroy'), { |
|
||||||
preserveScroll: true, |
|
||||||
onSuccess: () => closeModal(), |
|
||||||
onError: () => passwordInput.value.focus(), |
|
||||||
onFinish: () => form.reset(), |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
const closeModal = () => { |
|
||||||
confirmingUserDeletion.value = false; |
|
||||||
|
|
||||||
form.clearErrors(); |
|
||||||
form.reset(); |
|
||||||
}; |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<section class="space-y-6"> |
|
||||||
<header> |
|
||||||
<h2 class="text-lg font-medium text-gray-900"> |
|
||||||
Delete Account |
|
||||||
</h2> |
|
||||||
|
|
||||||
<p class="mt-1 text-sm text-gray-600"> |
|
||||||
Once your account is deleted, all of its resources and data will |
|
||||||
be permanently deleted. Before deleting your account, please |
|
||||||
download any data or information that you wish to retain. |
|
||||||
</p> |
|
||||||
</header> |
|
||||||
|
|
||||||
<DangerButton @click="confirmUserDeletion">Delete Account</DangerButton> |
|
||||||
|
|
||||||
<Modal :show="confirmingUserDeletion" @close="closeModal"> |
|
||||||
<div class="p-6"> |
|
||||||
<h2 |
|
||||||
class="text-lg font-medium text-gray-900" |
|
||||||
> |
|
||||||
Are you sure you want to delete your account? |
|
||||||
</h2> |
|
||||||
|
|
||||||
<p class="mt-1 text-sm text-gray-600"> |
|
||||||
Once your account is deleted, all of its resources and data |
|
||||||
will be permanently deleted. Please enter your password to |
|
||||||
confirm you would like to permanently delete your account. |
|
||||||
</p> |
|
||||||
|
|
||||||
<div class="mt-6"> |
|
||||||
<InputLabel |
|
||||||
for="password" |
|
||||||
value="Password" |
|
||||||
class="sr-only" |
|
||||||
/> |
|
||||||
|
|
||||||
<TextInput |
|
||||||
id="password" |
|
||||||
ref="passwordInput" |
|
||||||
v-model="form.password" |
|
||||||
type="password" |
|
||||||
class="mt-1 block w-3/4" |
|
||||||
placeholder="Password" |
|
||||||
@keyup.enter="deleteUser" |
|
||||||
/> |
|
||||||
|
|
||||||
<InputError :message="form.errors.password" class="mt-2" /> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="mt-6 flex justify-end"> |
|
||||||
<SecondaryButton @click="closeModal"> |
|
||||||
Cancel |
|
||||||
</SecondaryButton> |
|
||||||
|
|
||||||
<DangerButton |
|
||||||
class="ms-3" |
|
||||||
:class="{ 'opacity-25': form.processing }" |
|
||||||
:disabled="form.processing" |
|
||||||
@click="deleteUser" |
|
||||||
> |
|
||||||
Delete Account |
|
||||||
</DangerButton> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</Modal> |
|
||||||
</section> |
|
||||||
</template> |
|
Loading…
Reference in New Issue