Initial commit: SIBU 2.0 MISSION
This commit is contained in:
716
frontend/src/views/AdminTaxis.vue
Normal file
716
frontend/src/views/AdminTaxis.vue
Normal file
@ -0,0 +1,716 @@
|
||||
<template>
|
||||
<div class="admin-taxis">
|
||||
<div class="header">
|
||||
<button class="back-link" @click="$router.push('/admin')">← Volver al Panel</button>
|
||||
<h1>Directorio de Taxis</h1>
|
||||
<button class="btn-primary" @click="openModal()">
|
||||
<span class="material-icons">add</span>
|
||||
Nuevo Taxi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="loading">Cargando directorio...</div>
|
||||
|
||||
<div v-else class="taxis-list">
|
||||
<div v-if="taxis.length > 0" class="taxis-grid">
|
||||
<div v-for="taxi in taxis" :key="taxi.id" class="taxi-card">
|
||||
<div class="card-header">
|
||||
<div class="taxi-info">
|
||||
<div class="avatar">
|
||||
<img v-if="taxi.image_url" :src="getImageUrl(taxi.image_url)" alt="Taxi">
|
||||
<span v-else class="material-icons">local_taxi</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{{ taxi.owner_name }}</h3>
|
||||
<p class="plate">{{ taxi.license_plate }}</p>
|
||||
<p class="phone">{{ taxi.phone_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn-icon" @click="openModal(taxi)" title="Editar">
|
||||
<span class="material-icons">edit</span>
|
||||
</button>
|
||||
<button class="btn-icon delete" @click="deleteTaxi(taxi)" title="Eliminar">
|
||||
<span class="material-icons">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="info-row">
|
||||
<span class="label">Zona:</span>
|
||||
<span>{{ taxi.corregimiento }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Horario:</span>
|
||||
<span>{{ getShiftLabel(taxi.shift) }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="taxi.cooperative">
|
||||
<span class="label">Cooperativa:</span>
|
||||
<span>{{ taxi.cooperative }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Rating:</span>
|
||||
<span class="rating">{{ taxi.rating || 5.0 }} ⭐</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Inglés:</span>
|
||||
<span>{{ taxi.english_speaking ? 'Sí' : 'No' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Estado:</span>
|
||||
<span :class="taxi.is_active ? 'status-active' : 'status-inactive'">
|
||||
{{ taxi.is_active ? 'Activo' : 'Inactivo' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<span class="material-icons">local_taxi</span>
|
||||
<p>No hay taxis registrados en el directorio</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for Create/Edit -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2>{{ editingTaxi ? 'Editar Taxi' : 'Nuevo Taxi' }}</h2>
|
||||
<button @click="closeModal">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="saveTaxi">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Nombre del Conductor *</label>
|
||||
<input v-model="taxiForm.owner_name" type="text" placeholder="Juan Pérez" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Teléfono *</label>
|
||||
<input v-model="taxiForm.phone_number" type="tel" placeholder="+507 1234-5678" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Placa del Vehículo *</label>
|
||||
<input v-model="taxiForm.license_plate" type="text" placeholder="CHI-1234" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Zona de Servicio *</label>
|
||||
<select v-model="taxiForm.corregimiento" required>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="Boquete">Boquete</option>
|
||||
<option value="David - Boquete">David - Boquete</option>
|
||||
<option value="Boquete - David">Boquete - David</option>
|
||||
<option value="Aeropuerto - Boquete">Aeropuerto - Boquete</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Horario *</label>
|
||||
<select v-model="taxiForm.shift" required>
|
||||
<option value="">Seleccionar...</option>
|
||||
<option value="dia">Día</option>
|
||||
<option value="tarde">Tarde</option>
|
||||
<option value="noche">Noche</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Cooperativa</label>
|
||||
<input v-model="taxiForm.cooperative" type="text" placeholder="Cooperativa Boquete">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Rating (1-5)</label>
|
||||
<input v-model.number="taxiForm.rating" type="number" min="1" max="5" step="0.1" placeholder="5.0">
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<input v-model="taxiForm.english_speaking" type="checkbox">
|
||||
<span>Habla Inglés</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<input v-model="taxiForm.is_active" type="checkbox">
|
||||
<span>Activo en el directorio</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label>Foto del Conductor</label>
|
||||
<input type="file" @change="handleFileChange" accept="image/*">
|
||||
<small>Opcional - Foto para el directorio público</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error-text">{{ error }}</p>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn-secondary" @click="closeModal">Cancelar</button>
|
||||
<button type="submit" class="btn-primary" :disabled="isSaving">
|
||||
{{ isSaving ? 'Guardando...' : 'Guardar' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { apiClient, API_URL } from '@/services/apiClient'
|
||||
|
||||
const isLoading = ref(false)
|
||||
const taxis = ref<any[]>([])
|
||||
const showModal = ref(false)
|
||||
const editingTaxi = ref<any>(null)
|
||||
const isSaving = ref(false)
|
||||
const error = ref('')
|
||||
const photoFile = ref<File | null>(null)
|
||||
|
||||
const taxiForm = reactive({
|
||||
owner_name: '',
|
||||
phone_number: '',
|
||||
license_plate: '',
|
||||
corregimiento: '',
|
||||
shift: '',
|
||||
cooperative: '',
|
||||
rating: 5.0,
|
||||
english_speaking: false,
|
||||
is_active: true
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadTaxis()
|
||||
})
|
||||
|
||||
async function loadTaxis() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await apiClient.get('/api/taxis', {
|
||||
params: { is_active: undefined } // Get all taxis
|
||||
})
|
||||
taxis.value = response.data
|
||||
} catch (e) {
|
||||
console.error('Error loading taxis:', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(taxi?: any) {
|
||||
if (taxi) {
|
||||
editingTaxi.value = taxi
|
||||
Object.assign(taxiForm, {
|
||||
owner_name: taxi.owner_name,
|
||||
phone_number: taxi.phone_number,
|
||||
license_plate: taxi.license_plate,
|
||||
corregimiento: taxi.corregimiento,
|
||||
shift: taxi.shift,
|
||||
cooperative: taxi.cooperative || '',
|
||||
rating: taxi.rating || 5.0,
|
||||
english_speaking: taxi.english_speaking || false,
|
||||
is_active: taxi.is_active
|
||||
})
|
||||
} else {
|
||||
editingTaxi.value = null
|
||||
Object.assign(taxiForm, {
|
||||
owner_name: '',
|
||||
phone_number: '',
|
||||
license_plate: '',
|
||||
corregimiento: '',
|
||||
shift: '',
|
||||
cooperative: '',
|
||||
rating: 5.0,
|
||||
english_speaking: false,
|
||||
is_active: true
|
||||
})
|
||||
}
|
||||
photoFile.value = null
|
||||
error.value = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
editingTaxi.value = null
|
||||
photoFile.value = null
|
||||
}
|
||||
|
||||
function handleFileChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files && target.files[0]) {
|
||||
photoFile.value = target.files[0]
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTaxi() {
|
||||
isSaving.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('owner_name', taxiForm.owner_name)
|
||||
formData.append('phone_number', taxiForm.phone_number)
|
||||
formData.append('license_plate', taxiForm.license_plate)
|
||||
formData.append('corregimiento', taxiForm.corregimiento)
|
||||
formData.append('shift', taxiForm.shift)
|
||||
formData.append('rating', String(taxiForm.rating))
|
||||
formData.append('english_speaking', String(taxiForm.english_speaking))
|
||||
formData.append('is_active', String(taxiForm.is_active))
|
||||
|
||||
if (taxiForm.cooperative) formData.append('cooperative', taxiForm.cooperative)
|
||||
if (photoFile.value) formData.append('image', photoFile.value)
|
||||
|
||||
if (editingTaxi.value) {
|
||||
// Update existing taxi
|
||||
await apiClient.put(`/api/taxis/${editingTaxi.value.id}`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
} else {
|
||||
// Create new taxi
|
||||
await apiClient.post('/api/taxis', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
closeModal()
|
||||
await loadTaxis()
|
||||
} catch (e: any) {
|
||||
error.value = e.response?.data?.detail || 'Error al guardar el taxi'
|
||||
console.error('Error saving taxi:', e)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTaxi(taxi: any) {
|
||||
if (!confirm(`¿Eliminar a ${taxi.owner_name} del directorio?`)) return
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/api/taxis/${taxi.id}`)
|
||||
await loadTaxis()
|
||||
} catch (e) {
|
||||
alert('Error al eliminar el taxi')
|
||||
console.error('Error deleting taxi:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function getShiftLabel(shift: string) {
|
||||
const labels: Record<string, string> = {
|
||||
'dia': 'Día',
|
||||
'tarde': 'Tarde',
|
||||
'noche': 'Noche'
|
||||
}
|
||||
return labels[shift] || shift
|
||||
}
|
||||
|
||||
function getImageUrl(path: string) {
|
||||
if (path.startsWith('http')) return path
|
||||
return `${API_URL}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-taxis {
|
||||
padding: 24px;
|
||||
background: var(--bg-primary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
background: var(--hover-bg);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
flex: 1;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading, .empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state .material-icons {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.taxis-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.taxi-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 2px 8px var(--shadow);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.taxi-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px var(--shadow);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.taxi-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar .material-icons {
|
||||
font-size: 32px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.taxi-info h3 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.plate {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: var(--bg-secondary);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
font-size: 0.9rem;
|
||||
margin: 4px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.phone {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-icon.delete:hover {
|
||||
background: #fee;
|
||||
border-color: #f44;
|
||||
color: #f44;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rating {
|
||||
color: #fee715;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
color: var(--accent-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--card-bg);
|
||||
border-radius: 16px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-header button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-header button:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-group.checkbox-group {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-group.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="tel"],
|
||||
.form-group input[type="number"],
|
||||
.form-group select {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.form-group input[type="file"] {
|
||||
padding: 8px;
|
||||
border: 1px dashed var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f44;
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background: #fee;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.taxis-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user