Files
BarangaySystem/resources/js/Pages/CreateProductStoreOwner.vue
2026-06-06 18:43:00 +08:00

881 lines
29 KiB
Vue

<script setup>
import { usePageTitle } from '../composables/Core/usePageTitle';
usePageTitle('Create Product');
import { ref, computed, onMounted, watch } from 'vue';
import axios from 'axios';
import { useNavigate } from '../composables/Core/useNavigate';
import { useModal } from '../composables/Core/useModal';
import { useFileUpload } from '../composables/useFileUpload.js';
import { useProductStore } from '../stores/product';
import CardSimple from '../Components/Core/CardSimple.vue';
import Dropzone from '../Components/Core/Dropzone.vue';
import FileImage from '../Components/Core/FileImage.vue';
import StockPhotoPicker from '../Components/Core/StockPhotoPicker.vue';
import LoadingSpinner from '../Components/LoadingSpinner.vue';
const { navigate } = useNavigate();
const modal = useModal();
const productStore = useProductStore();
const { uploadFile, removeHash, photoHashes, isUploading: isFileUploading, uploadError } = useFileUpload({
category: 'ProductMarket',
maxSizeMB: 10,
});
const STEP = {
PICK: 1,
NEW_GLOBAL: 2,
DESCRIPTION: 3,
ASSIGN_STORES: 4,
PER_STORE: 5,
};
const step = ref(STEP.PICK);
const isLoading = ref(false);
const isSubmitting = ref(false);
const error = ref(null);
// Stores
const selectableStores = ref([]);
const noStoresChecked = ref(false);
// Flow mode
const mode = ref('existing'); // 'existing' | 'new'
// Selected existing global product
const pickedProduct = ref(null);
// Search
const searchTerm = ref('');
const searchResults = ref([]);
const isSearching = ref(false);
let searchDebounce = null;
// New global product form
const newProduct = ref({
name: '',
description: '',
category: '',
subcategory: '',
price: 1,
unitname: '',
available: 1,
barcode: '',
});
const categoryList = ref([]);
const subcategoryList = ref([]);
const dropzoneRef = ref(null);
const dropzoneFiles = ref([]);
// Stock photo picker
const showPhotoPicker = ref(false);
const onStockPhotoSelected = ({ hashkey, url }) => {
// Mirror Dropzone's entry shape: preview drives the thumbnail, hashkey is
// what the submit handler filters on for the photourl payload.
dropzoneFiles.value.push({ file: null, name: 'stock-photo.jpg', preview: url, hashkey, uploading: false, progress: 100, error: null });
};
// Description override (existing path)
const overrideDescription = ref('');
// Store selection + per-store overrides
const assignedStoreHashes = ref([]); // array of store hashkeys
const perStoreOverrides = ref({}); // { [storeHash]: { price, available } }
// ---------------- Bootstrap ----------------
onMounted(async () => {
isLoading.value = true;
await fetchSelectableStores();
isLoading.value = false;
noStoresChecked.value = true;
if (selectableStores.value.length === 0) {
modal.yesNoModal({
title: 'No store found',
body: 'You need to create a store before you can add a product. Create one now?',
yesText: 'Create Store',
onYes: () => navigate({ page: 'CreateStore' }),
noText: 'Cancel',
onNo: () => navigate({ page: 'Home' }),
});
} else {
loadCategories();
}
});
const fetchSelectableStores = async () => {
try {
const { data } = await axios.post('/Admin/Stores/Selectable');
if (data && data.success) selectableStores.value = data.data || [];
} catch (e) {
console.error('Failed to load stores', e);
}
};
const loadCategories = async () => {
try {
const { data } = await axios.post('/Products/New/Category/Datalist', {});
if (Array.isArray(data)) {
categoryList.value = data.map((item) => ({
value: typeof item === 'string' ? item : item[0],
label: typeof item === 'string' ? item : item[1] || item[0],
}));
}
} catch (e) {
console.error('Failed to load categories', e);
}
};
const loadSubcategories = async () => {
if (!newProduct.value.category) {
subcategoryList.value = [];
return;
}
try {
const { data } = await axios.post('/Products/New/SubCategory/Datalist', {
category: newProduct.value.category,
});
if (Array.isArray(data)) {
subcategoryList.value = data.map((item) => ({
value: typeof item === 'string' ? item : item[0],
label: typeof item === 'string' ? item : item[1] || item[0],
}));
}
} catch (e) {
console.error('Failed to load subcategories', e);
}
};
watch(() => newProduct.value.category, loadSubcategories);
// ---------------- Search ----------------
watch(searchTerm, (val) => {
clearTimeout(searchDebounce);
if (!val || val.trim().length < 2) {
searchResults.value = [];
return;
}
searchDebounce = setTimeout(runSearch, 300);
});
const runSearch = async () => {
if (!searchTerm.value || searchTerm.value.trim().length < 2) return;
isSearching.value = true;
try {
const { data } = await axios.post('/Products/Admin/FuzzySearch', {
name: searchTerm.value.trim(),
});
searchResults.value = data && data.success && Array.isArray(data.data) ? data.data : [];
} catch (e) {
console.error('Search failed', e);
searchResults.value = [];
} finally {
isSearching.value = false;
}
};
// ---------------- Dropzone ----------------
watch(
() => dropzoneFiles.value,
async (newFiles) => {
const filesToUpload = newFiles.filter((f) => !f.uploading && !f.hashkey && !f.error);
for (const fileObj of filesToUpload) {
const idx = newFiles.indexOf(fileObj);
if (idx === -1) continue;
dropzoneRef.value.setFileStatus(idx, { uploading: true, progress: 30 });
const result = await uploadFile(fileObj.file);
if (result && result.hashkey) {
dropzoneRef.value.setFileStatus(idx, { uploading: false, progress: 100, hashkey: result.hashkey });
if (error.value && error.value.startsWith('Photo upload failed:')) {
error.value = null;
}
} else {
const msg = uploadError.value || 'Upload failed';
dropzoneRef.value.setFileStatus(idx, { uploading: false, progress: 0, error: msg });
error.value = `Photo upload failed: ${msg}`;
}
}
},
{ deep: true }
);
const handlePhotoRemoved = (hashkey) => {
if (hashkey) removeHash(hashkey);
};
// ---------------- Step transitions ----------------
const selectExistingProduct = (product) => {
pickedProduct.value = product;
mode.value = 'existing';
overrideDescription.value = product.description || '';
step.value = STEP.DESCRIPTION;
error.value = null;
};
const startNewProduct = () => {
mode.value = 'new';
pickedProduct.value = null;
step.value = STEP.NEW_GLOBAL;
error.value = null;
};
const validateNewProduct = () => {
const p = newProduct.value;
if (!p.name) return 'Product name is required';
if (!p.description) return 'Description is required';
if (!p.category) return 'Category is required';
if (!p.subcategory) return 'Subcategory is required';
if (!p.price || parseFloat(p.price) <= 0) return 'Valid price is required';
if (!p.unitname) return 'Unit name is required';
const hasPhoto = dropzoneFiles.value.some((f) => !!f.hashkey);
if (!hasPhoto) return 'At least one photo is required';
if (p.barcode && !/^\d{12}$/.test(p.barcode)) return 'Barcode must be exactly 12 digits';
return null;
};
const advanceFromNewGlobal = () => {
const err = validateNewProduct();
if (err) {
error.value = err;
return;
}
error.value = null;
step.value = STEP.ASSIGN_STORES;
};
const advanceFromDescription = () => {
if (!overrideDescription.value || !overrideDescription.value.trim()) {
error.value = 'Description is required';
return;
}
error.value = null;
step.value = STEP.ASSIGN_STORES;
};
const toggleStore = (hash) => {
const i = assignedStoreHashes.value.indexOf(hash);
if (i >= 0) {
assignedStoreHashes.value.splice(i, 1);
delete perStoreOverrides.value[hash];
} else {
assignedStoreHashes.value.push(hash);
}
};
const advanceFromStores = () => {
if (assignedStoreHashes.value.length === 0) {
error.value = 'Select at least one store.';
return;
}
// Seed per-store defaults from the global product.
const defaultPrice = mode.value === 'new'
? parseFloat(newProduct.value.price) || 0
: parseFloat(pickedProduct.value?.price) || 0;
const defaultAvailable = mode.value === 'new'
? parseInt(newProduct.value.available) || 1
: 1;
for (const hash of assignedStoreHashes.value) {
if (!perStoreOverrides.value[hash]) {
perStoreOverrides.value[hash] = {
price: defaultPrice,
available: defaultAvailable,
};
}
}
error.value = null;
step.value = STEP.PER_STORE;
};
const goBack = () => {
error.value = null;
if (step.value === STEP.PER_STORE) {
step.value = STEP.ASSIGN_STORES;
} else if (step.value === STEP.ASSIGN_STORES) {
step.value = mode.value === 'new' ? STEP.NEW_GLOBAL : STEP.DESCRIPTION;
} else if (step.value === STEP.DESCRIPTION || step.value === STEP.NEW_GLOBAL) {
step.value = STEP.PICK;
}
};
// ---------------- Submit ----------------
const submit = async () => {
if (isSubmitting.value) return;
for (const hash of assignedStoreHashes.value) {
const ov = perStoreOverrides.value[hash];
if (!ov || !ov.price || parseFloat(ov.price) <= 0) {
error.value = 'Each assigned store needs a valid price.';
return;
}
if (ov.available === '' || ov.available === null || parseInt(ov.available) < 0) {
error.value = 'Each assigned store needs a valid availability.';
return;
}
}
isSubmitting.value = true;
error.value = null;
try {
let productHash = pickedProduct.value?.hashkey;
let storesToAssign = [...assignedStoreHashes.value];
if (mode.value === 'new') {
const firstStore = storesToAssign[0];
const photoHashList = dropzoneFiles.value.filter((f) => f.hashkey).map((f) => f.hashkey);
const { data } = await axios.post('/Products/Admin/New/', {
NewProductName: newProduct.value.name,
NewProductDescription: newProduct.value.description,
NewProductCategory: newProduct.value.category,
NewProductSubCategory: newProduct.value.subcategory,
NewProductPrice: parseFloat(newProduct.value.price),
NewProductUnitName: newProduct.value.unitname,
NewProductAvailable: parseInt(newProduct.value.available),
NewProductBarcode: newProduct.value.barcode,
TargetStore: firstStore,
photourl: photoHashList,
});
if (!data || !data.success) {
error.value = data?.message || 'Failed to create global product.';
isSubmitting.value = false;
return;
}
productHash = data.data?.hashkey || data.hashkey;
}
const descriptionOverride =
mode.value === 'new' ? newProduct.value.description : overrideDescription.value;
const failures = [];
for (const hash of storesToAssign) {
const ov = perStoreOverrides.value[hash];
try {
await axios.post('/Products/AssignToStore/', {
target: productHash,
TargetStore: hash,
price: parseFloat(ov.price),
available: parseInt(ov.available),
description: descriptionOverride,
});
} catch (e) {
const storeName = selectableStores.value.find((s) => s.hashkey === hash)?.name || hash;
failures.push(storeName);
}
}
if (failures.length === storesToAssign.length) {
error.value = 'Failed to assign product to any selected store.';
isSubmitting.value = false;
return;
}
productStore.fetchProducts();
modal.quickDismiss({
title: 'Product Listed',
body:
failures.length > 0
? `Listed in ${storesToAssign.length - failures.length} store(s). Failed for: ${failures.join(', ')}.`
: `Your product is now listed in ${storesToAssign.length} store(s).`,
onShown: () => {
setTimeout(() => navigate({ page: 'ManageProductsAdmin' }), 1200);
},
});
} catch (e) {
console.error('Submit failed', e);
error.value = e.response?.data?.message || 'Failed to create product.';
} finally {
isSubmitting.value = false;
}
};
// ---------------- Helpers ----------------
const stepTitle = computed(() => {
switch (step.value) {
case STEP.PICK:
return 'Find your product';
case STEP.NEW_GLOBAL:
return 'Create new product';
case STEP.DESCRIPTION:
return 'Describe your product';
case STEP.ASSIGN_STORES:
return 'Assign to stores';
case STEP.PER_STORE:
return 'Price & availability per store';
default:
return '';
}
});
const stepNumber = computed(() => {
if (step.value === STEP.PICK) return 1;
if (step.value === STEP.NEW_GLOBAL || step.value === STEP.DESCRIPTION) return 2;
if (step.value === STEP.ASSIGN_STORES) return 3;
if (step.value === STEP.PER_STORE) return 4;
return 1;
});
const storeName = (hash) =>
selectableStores.value.find((s) => s.hashkey === hash)?.name || hash;
const globalDefaultPrice = computed(() =>
mode.value === 'new'
? parseFloat(newProduct.value.price) || 0
: parseFloat(pickedProduct.value?.price) || 0
);
</script>
<template>
<div class="csop-page">
<div class="tf-container mt-4 mb-3 text-center">
<h1 class="fw_8 page-title">Add a Product to Your Store</h1>
<p class="text-muted small mb-0">Step {{ stepNumber }} of 4 {{ stepTitle }}</p>
</div>
<div v-if="error" class="tf-container mb-3">
<div class="glass-alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
{{ error }}
</div>
</div>
<div v-if="isLoading" class="tf-container text-center py-5">
<LoadingSpinner />
</div>
<div v-else-if="noStoresChecked && selectableStores.length === 0" class="tf-container text-center py-5">
<p class="text-muted">You need a store before adding products.</p>
<button class="btn btn-primary rounded-pill px-4 mt-2" @click="navigate({ page: 'CreateStore' })">
Create a Store
</button>
</div>
<div v-else class="tf-container">
<!-- STEP 1: Pick existing or new -->
<div v-if="step === STEP.PICK">
<CardSimple title="Search for your product" cardStyle="height: auto">
<p class="text-muted small">
Many products are already in our system. Type a product name to see if yours exists.
</p>
<div class="premium-input-group mb-3">
<input
type="text"
v-model="searchTerm"
class="premium-input"
placeholder="e.g., Premium Rice"
autofocus
/>
</div>
<div v-if="isSearching" class="text-center py-3">
<LoadingSpinner size="small" />
</div>
<div v-else-if="searchResults.length > 0" class="results-list">
<div
v-for="m in searchResults"
:key="m.hashkey"
class="result-row"
@click="selectExistingProduct(m)"
>
<FileImage
:src="m.photourl && m.photourl[0] ? m.photourl[0] : ''"
:alt="m.name"
class="result-thumb"
fallback="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/146710fe9ece.bin"
/>
<div class="result-info">
<div class="fw_6">{{ m.name }}</div>
<div class="text-muted smallest">
<span v-if="m.category">{{ m.category }}<span v-if="m.subcategory"> · {{ m.subcategory }}</span> · </span>
<span>{{ m.price }} / {{ m.unitname }}</span>
</div>
</div>
<i class="fas fa-chevron-right text-muted"></i>
</div>
</div>
<div
v-else-if="searchTerm && searchTerm.length >= 2"
class="text-muted smallest text-center py-3"
>
No matches yet. You can create a new product below.
</div>
</CardSimple>
<div class="text-center mt-4">
<button class="btn btn-outline-primary rounded-pill px-4" @click="startNewProduct">
<i class="fas fa-plus me-2"></i> My product is not listed Create new
</button>
</div>
<div class="text-center mt-3">
<button class="btn-text" @click="navigate({ page: 'Home' })">
<i class="fas fa-chevron-left me-2"></i> Cancel
</button>
</div>
</div>
<!-- STEP 2 (NEW): Full new global product form -->
<div v-else-if="step === STEP.NEW_GLOBAL">
<CardSimple title="New product details">
<div class="premium-input-group mb-3">
<label class="form-label">Product Name <span class="required">*</span></label>
<input type="text" v-model="newProduct.name" class="premium-input" placeholder="e.g., Premium Rice" />
</div>
<div class="premium-input-group mb-3">
<label class="form-label">Description <span class="required">*</span></label>
<textarea v-model="newProduct.description" class="premium-input" rows="3" placeholder="Describe your product..."></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="premium-input-group mb-3">
<label class="form-label">Category <span class="required">*</span></label>
<select v-model="newProduct.category" class="premium-select">
<option value="" disabled>Select Category</option>
<option v-for="cat in categoryList" :key="cat.value" :value="cat.value">{{ cat.label }}</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="premium-input-group mb-3">
<label class="form-label">Subcategory <span class="required">*</span></label>
<select v-model="newProduct.subcategory" class="premium-select" :disabled="subcategoryList.length === 0">
<option value="" disabled>Select Subcategory</option>
<option v-for="sub in subcategoryList" :key="sub.value" :value="sub.value">{{ sub.label }}</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="premium-input-group mb-3">
<label class="form-label">Base Price (PHP) <span class="required">*</span></label>
<input type="number" v-model="newProduct.price" class="premium-input" min="1" step="0.01" />
</div>
</div>
<div class="col-md-4">
<div class="premium-input-group mb-3">
<label class="form-label">Unit <span class="required">*</span></label>
<input type="text" v-model="newProduct.unitname" class="premium-input" placeholder="e.g., 25kg" />
</div>
</div>
<div class="col-md-4">
<div class="premium-input-group mb-3">
<label class="form-label">Default Available <span class="required">*</span></label>
<input type="number" v-model="newProduct.available" class="premium-input" min="1" />
</div>
</div>
</div>
<div class="premium-input-group mb-3">
<label class="form-label">Barcode (12 digits)</label>
<input type="text" v-model="newProduct.barcode" class="premium-input" maxlength="12" placeholder="Optional" />
</div>
<div class="premium-input-group">
<label class="form-label">Product Photos <span class="required">*</span></label>
<button type="button" class="btn btn-outline-secondary btn-sm rounded-pill mb-2"
@click="showPhotoPicker = true">
<i class="fas fa-images me-1"></i> Search Stock Photos
</button>
<Dropzone ref="dropzoneRef" v-model:files="dropzoneFiles" @removed="handlePhotoRemoved" />
<StockPhotoPicker v-model="showPhotoPicker" :product-name="newProduct.name"
@photo-selected="onStockPhotoSelected" />
</div>
</CardSimple>
<div class="nav-bar mt-4">
<button class="btn-text" @click="goBack">
<i class="fas fa-chevron-left me-2"></i> Back
</button>
<button
class="btn btn-primary rounded-pill px-4"
:disabled="isFileUploading"
@click="advanceFromNewGlobal"
>
Next <i class="fas fa-chevron-right ms-2"></i>
</button>
</div>
</div>
<!-- STEP 2 (EXISTING): Description override -->
<div v-else-if="step === STEP.DESCRIPTION">
<CardSimple :title="pickedProduct?.name || 'Selected product'">
<div class="picked-preview mb-3">
<FileImage
:src="pickedProduct?.photourl && pickedProduct.photourl[0] ? pickedProduct.photourl[0] : ''"
:alt="pickedProduct?.name"
class="picked-thumb"
fallback="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/146710fe9ece.bin"
/>
<div class="text-muted small">
<span v-if="pickedProduct?.category">{{ pickedProduct.category }}<span v-if="pickedProduct.subcategory"> · {{ pickedProduct.subcategory }}</span> · </span>
<span>{{ pickedProduct?.price }} / {{ pickedProduct?.unitname }}</span>
</div>
</div>
<div class="premium-input-group">
<label class="form-label">Description for your listing <span class="required">*</span></label>
<textarea
v-model="overrideDescription"
class="premium-input"
rows="5"
placeholder="Describe how this product appears in your store..."
></textarea>
<p class="smallest text-muted mt-2">
<i class="fas fa-info-circle me-1"></i>
This description will be shown for this product across the stores you assign it to.
</p>
</div>
</CardSimple>
<div class="nav-bar mt-4">
<button class="btn-text" @click="goBack">
<i class="fas fa-chevron-left me-2"></i> Back
</button>
<button class="btn btn-primary rounded-pill px-4" @click="advanceFromDescription">
Next <i class="fas fa-chevron-right ms-2"></i>
</button>
</div>
</div>
<!-- STEP 3: Assign to stores -->
<div v-else-if="step === STEP.ASSIGN_STORES">
<CardSimple title="Which of your stores should sell this?">
<p class="text-muted small">Pick one or more stores.</p>
<div class="store-list">
<label
v-for="s in selectableStores"
:key="s.hashkey"
class="store-row"
:class="{ 'is-selected': assignedStoreHashes.includes(s.hashkey) }"
>
<input
type="checkbox"
:checked="assignedStoreHashes.includes(s.hashkey)"
@change="toggleStore(s.hashkey)"
/>
<div>
<div class="fw_6">{{ s.name }}</div>
<div class="text-muted smallest">{{ s.role }}<span v-if="s.category"> · {{ s.category }}</span></div>
</div>
</label>
</div>
</CardSimple>
<div class="nav-bar mt-4">
<button class="btn-text" @click="goBack">
<i class="fas fa-chevron-left me-2"></i> Back
</button>
<button class="btn btn-primary rounded-pill px-4" @click="advanceFromStores">
Next <i class="fas fa-chevron-right ms-2"></i>
</button>
</div>
</div>
<!-- STEP 4: Per-store overrides -->
<div v-else-if="step === STEP.PER_STORE">
<CardSimple title="Set price and stock for each store">
<p class="text-muted small">
Defaults come from the product's base price (₱{{ globalDefaultPrice }}). Adjust per store as needed.
</p>
<div class="per-store-list">
<div
v-for="hash in assignedStoreHashes"
:key="hash"
class="per-store-row"
>
<div class="per-store-name">
<i class="fas fa-store me-2 text-muted"></i>
<span class="fw_6">{{ storeName(hash) }}</span>
</div>
<div class="per-store-fields">
<div class="premium-input-group">
<label class="form-label smallest">Price (PHP)</label>
<input
type="number"
class="premium-input"
min="1"
step="0.01"
v-model="perStoreOverrides[hash].price"
/>
</div>
<div class="premium-input-group">
<label class="form-label smallest">Available</label>
<input
type="number"
class="premium-input"
min="0"
v-model="perStoreOverrides[hash].available"
/>
</div>
</div>
</div>
</div>
</CardSimple>
<div class="nav-bar mt-4">
<button class="btn-text" @click="goBack">
<i class="fas fa-chevron-left me-2"></i> Back
</button>
<AnimatedButton
@click="submit"
:disabled="isSubmitting"
:loading="isSubmitting"
btnClass="btn-premium-launch"
>
List Product
</AnimatedButton>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.csop-page {
padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
}
.page-title {
font-family: 'Outfit', sans-serif;
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.premium-input-group { display: flex; flex-direction: column; }
.form-label { font-weight: 600; font-size: 0.9rem; color: #475569; margin-bottom: 6px; }
.required { color: #ef4444; margin-left: 4px; }
.premium-input, .premium-select {
padding: 12px 16px;
border-radius: 12px;
border: 1px solid #e2e8f0;
background: #fff;
font-size: 0.95rem;
outline: none;
transition: all 0.2s;
}
.premium-input:focus, .premium-select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
}
.premium-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
}
.glass-alert {
padding: 14px 18px;
border-radius: 14px;
font-weight: 500;
display: flex;
align-items: center;
}
.alert-danger {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
color: #b91c1c;
}
.results-list { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.result-row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid #e2e8f0;
border-radius: 12px;
cursor: pointer;
transition: all 0.15s;
}
.result-row:hover { border-color: #93c5fd; background: rgba(59, 130, 246, 0.04); }
.result-thumb {
width: 48px; height: 48px; border-radius: 10px;
object-fit: cover; flex-shrink: 0;
}
.result-info { flex: 1; min-width: 0; }
.picked-preview { display: flex; align-items: center; gap: 12px; }
.picked-thumb { width: 72px; height: 72px; border-radius: 12px; object-fit: cover; }
.store-list { display: flex; flex-direction: column; gap: 8px; }
.store-row {
display: flex; align-items: center; gap: 12px;
padding: 12px 14px;
border: 1px solid #e2e8f0;
border-radius: 12px;
cursor: pointer;
transition: all 0.15s;
}
.store-row:hover { border-color: #93c5fd; }
.store-row.is-selected { border-color: #2563eb; background: rgba(37, 99, 235, 0.06); }
.per-store-list { display: flex; flex-direction: column; gap: 12px; }
.per-store-row {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
.per-store-name { display: flex; align-items: center; }
.per-store-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-text {
background: transparent;
border: none;
color: #64748b;
font-weight: 500;
cursor: pointer;
}
.btn-text:hover { color: #1e293b; }
.btn-premium-launch {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
border: none;
padding: 12px 32px;
border-radius: 14px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.3);
}
.btn-premium-launch:disabled { background: #cbd5e1; cursor: not-allowed; box-shadow: none; }
:global(.dark-mode) .premium-input,
:global(.dark-mode) .premium-select {
background: #1e293b;
border-color: #334155;
color: #f8fafc;
}
:global(.dark-mode) .form-label { color: #94a3b8; }
:global(.dark-mode) .result-row,
:global(.dark-mode) .store-row,
:global(.dark-mode) .per-store-row { border-color: #334155; }
:global(.dark-mode) .page-title {
background: linear-gradient(135deg, #f8fafc 0%, #cbd5e1 100%);
-webkit-background-clip: text;
background-clip: text;
}
</style>