#51 feat: logo del sitio editable desde el panel de administración

This commit is contained in:
2026-08-08 14:19:56 -05:00
parent 5aaaac1e45
commit 24fa4800a9
7 changed files with 157 additions and 22 deletions

View File

@@ -7,12 +7,7 @@
<div class="glow-bubble bubble-red"></div> <div class="glow-bubble bubble-red"></div>
<div class="login-card"> <div class="login-card">
<v-img <SiteLogo max-width="140" class="mx-auto mb-4" />
:src="logo"
alt="Don Confiao"
max-width="140"
class="mx-auto mb-4"
/>
<h1 class="text-h5 text-sm-h4 font-weight-bold text-center mb-1"> <h1 class="text-h5 text-sm-h4 font-weight-bold text-center mb-1">
Iniciar Sesión Iniciar Sesión
</h1> </h1>
@@ -74,7 +69,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import AuthService from '@/services/auth' import AuthService from '@/services/auth'
import logo from '@/assets/logo_colorful.png' import SiteLogo from '@/components/SiteLogo.vue'
const router = useRouter() const router = useRouter()

View File

@@ -0,0 +1,39 @@
<template>
<v-img
v-if="logoUrl && !broken"
:src="logoUrl"
:alt="alt"
:max-width="maxWidth"
@error="broken = true"
/>
</template>
<script setup>
import { computed, inject, onMounted, ref } from 'vue';
import { useSettingsStore } from '@/stores/settings';
const props = defineProps({
maxWidth: {
type: [Number, String],
default: 180,
},
alt: {
type: String,
default: 'Don Confiao',
},
});
const settingsStore = useSettingsStore();
const api = inject('api');
const broken = ref(false);
const logoUrl = computed(() => settingsStore.logo);
onMounted(async () => {
try {
await settingsStore.fetchSettings(api);
} catch (e) {
broken.value = true;
}
});
</script>

View File

@@ -61,10 +61,12 @@
import { ref, computed, inject, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { ref, computed, inject, onMounted, onBeforeUnmount, nextTick } from 'vue';
import L from 'leaflet'; import L from 'leaflet';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import { useSettingsStore } from '@/stores/settings';
const api = inject('api'); const api = inject('api');
const settingsStore = useSettingsStore();
const settings = ref(null); const settings = computed(() => settingsStore.settings);
const mapEl = ref(null); const mapEl = ref(null);
let map = null; let map = null;
@@ -125,7 +127,7 @@ function handleResize() {
onMounted(async () => { onMounted(async () => {
try { try {
settings.value = await api.getStoreSettings(); await settingsStore.fetchSettings(api);
await nextTick(); await nextTick();
initMap(); initMap();
} catch (error) { } catch (error) {

View File

@@ -60,6 +60,39 @@
/> />
</v-col> </v-col>
</v-row> </v-row>
<v-divider class="my-4" />
<div>
<h3 class="text-h6 mb-2">Logo del sitio</h3>
<v-img
v-if="currentLogo"
:src="currentLogo"
alt="Logo actual"
max-width="180"
class="mb-3"
/>
<v-file-input
v-model="logoFile"
label="Seleccionar nueva imagen del logo"
accept="image/*"
prepend-icon="mdi-image"
variant="outlined"
class="mb-3"
clearable
/>
<v-btn
v-if="currentLogo"
variant="tonal"
color="error"
prepend-icon="mdi-image-off"
:loading="removingLogo"
:disabled="removingLogo || submitting"
@click="removeLogo"
>
Quitar logo actual
</v-btn>
</div>
</v-form> </v-form>
</v-card-text> </v-card-text>
</v-card> </v-card>
@@ -82,11 +115,16 @@
<script setup> <script setup>
import { ref, inject, onMounted } from "vue"; import { ref, inject, onMounted } from "vue";
import { useSettingsStore } from "@/stores/settings";
const api = inject("api"); const api = inject("api");
const settingsStore = useSettingsStore();
const formRef = ref(null); const formRef = ref(null);
const submitting = ref(false); const submitting = ref(false);
const removingLogo = ref(false);
const logoFile = ref(null);
const currentLogo = ref(null);
const form = ref({ const form = ref({
address: "", address: "",
@@ -99,11 +137,13 @@ const snackbar = ref({ show: false, message: "", color: "success" });
async function loadSettings() { async function loadSettings() {
try { try {
const data = await api.getStoreSettings(); const data = await api.getStoreSettings();
settingsStore.setSettings(data);
form.value = { form.value = {
address: data.address || "", address: data.address || "",
latitude: data.latitude ?? null, latitude: data.latitude ?? null,
longitude: data.longitude ?? null, longitude: data.longitude ?? null,
}; };
currentLogo.value = data.logo || null;
} catch (error) { } catch (error) {
console.error("Error al cargar la información de la tienda:", error); console.error("Error al cargar la información de la tienda:", error);
showSnackbar("Error al cargar la información de la tienda", "error"); showSnackbar("Error al cargar la información de la tienda", "error");
@@ -119,11 +159,26 @@ async function submitForm() {
submitting.value = true; submitting.value = true;
try { try {
await api.updateStoreSettings({ if (logoFile.value) {
address: form.value.address, const payload = new FormData();
latitude: toNumberOrNull(form.value.latitude), payload.append("address", form.value.address);
longitude: toNumberOrNull(form.value.longitude), if (form.value.latitude !== "" && form.value.latitude != null) {
}); payload.append("latitude", toNumberOrNull(form.value.latitude));
}
if (form.value.longitude !== "" && form.value.longitude != null) {
payload.append("longitude", toNumberOrNull(form.value.longitude));
}
payload.append("logo", logoFile.value);
await api.updateStoreSettings(payload);
} else {
await api.updateStoreSettings({
address: form.value.address,
latitude: toNumberOrNull(form.value.latitude),
longitude: toNumberOrNull(form.value.longitude),
});
}
logoFile.value = null;
await refreshLogo();
showSnackbar("Información de la tienda actualizada", "success"); showSnackbar("Información de la tienda actualizada", "success");
} catch (error) { } catch (error) {
console.error("Error al guardar la información de la tienda:", error); console.error("Error al guardar la información de la tienda:", error);
@@ -133,6 +188,27 @@ async function submitForm() {
} }
} }
async function removeLogo() {
removingLogo.value = true;
try {
await api.updateStoreSettings({ logo: null });
logoFile.value = null;
await refreshLogo();
showSnackbar("Logo eliminado", "success");
} catch (error) {
console.error("Error al quitar el logo:", error);
showSnackbar("Error al quitar el logo", "error");
} finally {
removingLogo.value = false;
}
}
async function refreshLogo() {
const data = await api.getStoreSettings();
settingsStore.setSettings(data);
currentLogo.value = data.logo || null;
}
function toNumberOrNull(value) { function toNumberOrNull(value) {
if (value === null || value === undefined || value === "") return null; if (value === null || value === undefined || value === "") return null;
const number = Number(value); const number = Number(value);

View File

@@ -8,12 +8,7 @@
<div class="glow-bubble bubble-yellow"></div> <div class="glow-bubble bubble-yellow"></div>
<div class="glow-bubble bubble-red"></div> <div class="glow-bubble bubble-red"></div>
<div class="hero-content"> <div class="hero-content">
<v-img <SiteLogo max-width="180" class="mx-auto mb-4" />
:src="logo"
alt="Don Confiao"
max-width="180"
class="mx-auto mb-4"
/>
<h1 class="text-h4 font-weight-bold mb-2">Don Confiao te atiende</h1> <h1 class="text-h4 font-weight-bold mb-2">Don Confiao te atiende</h1>
<p class="text-subtitle-1 font-italic font-weight-bold"> <p class="text-subtitle-1 font-italic font-weight-bold">
Economía solidaria, mercado justo, alimentación sana Economía solidaria, mercado justo, alimentación sana
@@ -122,8 +117,8 @@
<script setup> <script setup>
import ResaltedText from "@/components/ResaltedText.vue"; import ResaltedText from "@/components/ResaltedText.vue";
import StoreLocation from "@/components/StoreLocation.vue"; import StoreLocation from "@/components/StoreLocation.vue";
import SiteLogo from "@/components/SiteLogo.vue";
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import logo from "@/assets/logo_colorful.png";
const authStore = useAuthStore(); const authStore = useAuthStore();
</script> </script>

View File

@@ -162,7 +162,9 @@ class DjangoApi {
updateStoreSettings(data) { updateStoreSettings(data) {
const url = this.base + "/don_confiao/api/store_settings"; const url = this.base + "/don_confiao/api/store_settings";
return this.patchRequest(url, data); return http.patch(url, data, {
headers: { 'Content-Type': undefined },
}).then((r) => r.data);
} }
} }

26
src/stores/settings.js Normal file
View File

@@ -0,0 +1,26 @@
import { defineStore } from 'pinia'
export const useSettingsStore = defineStore('settings', {
state: () => ({
settings: null,
loaded: false
}),
getters: {
logo: (state) => state.settings?.logo || null,
address: (state) => state.settings?.address || '',
latitude: (state) => state.settings?.latitude,
longitude: (state) => state.settings?.longitude
},
actions: {
async fetchSettings(api) {
if (this.loaded) return this.settings
this.settings = await api.getStoreSettings()
this.loaded = true
return this.settings
},
setSettings(settings) {
this.settings = settings
this.loaded = true
}
}
})