Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 91 additions & 29 deletions kopiCrumbs/src/views/vendor/VendorRegisterPage.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="auth-page">
<img src="@/assets/logo.png" alt="Logo" class="auth-logo" />

<div class="auth-card card">
<h2>Register Your Business</h2>
<p class="auth-subtitle">Join {{ APP_DISPLAY_NAME }} as a vendor</p>
Expand All @@ -14,84 +14,141 @@
<FormInput v-model="form.contactNumber" label="Contact Number" placeholder="+65 XXXX XXXX" />
<FormInput v-model="form.locationText" label="Store Location" placeholder="e.g. Toa Payoh Block 123" />
<FormSelect v-model="form.region" label="Region" :options="REGIONS" placeholder="Select region" required :error="errors.region" />

<div class="coords-row">
<FormInput v-model.number="form.lat" label="Latitude" type="number" placeholder="1.3521" />
<FormInput v-model.number="form.lng" label="Longitude" type="number" placeholder="103.8198" />

<!-- Coords toggle -->
<div class="form-group">
<label class="form-label">Coordinates</label>
<div class="coords-toggle">
<button type="button" class="btn btn-secondary btn-sm" @click="togglePostalInput">
{{ usePostalInput ? 'Enter Lat/Lng manually' : 'Use Postal Code instead' }}
</button>
</div>

<div v-if="usePostalInput" style="margin-top: var(--space-sm);">
<label class="form-label">Postal Code</label>
<div class="postal-row">
<input
v-model="postalCode"
class="form-input"
placeholder="e.g. 238823"
maxlength="6"
/>
<button type="button" class="btn btn-primary btn-sm" @click="handlePostalLookup">
Look Up
</button>
</div>
<p v-if="postalError" class="form-error">{{ postalError }}</p>
<p v-if="form.lat && form.lng" class="postal-success">
✓ Found: {{ Number(form.lat).toFixed(4) }}, {{ Number(form.lng).toFixed(4) }}
</p>
</div>

<div v-else class="coords-row" style="margin-top: var(--space-sm);">
<FormInput v-model.number="form.lat" label="Latitude" type="number" placeholder="1.3521" />
<FormInput v-model.number="form.lng" label="Longitude" type="number" placeholder="103.8198" />
</div>
</div>

<h3 class="section-label">Social Links (optional)</h3>
<FormInput v-model="form.instagram" label="Instagram" placeholder="handle (without @)" />
<FormInput v-model="form.tiktok" label="TikTok" placeholder="handle (without @)" />
<FormInput v-model="form.lemon8" label="Lemon8" placeholder="handle" />

<MultiSelectChips v-model="form.categories" label="Food Categories" :options="FOOD_CATEGORIES" />
<MultiSelectChips v-model="form.cuisineTags" label="Cuisine Tags" :options="CUISINE_TAGS" />
<MultiSelectChips v-model="form.dietaryTags" label="Dietary Tags" :options="DIETARY_TAGS" />

<p v-if="authError" class="form-error auth-error">{{ authError }}</p>

<button type="submit" class="btn btn-primary btn-full" :disabled="submitting">
{{ submitting ? 'Registering...' : 'Register Business' }}
</button>
</form>

<div class="auth-links">
<p>Already registered? <router-link to="/vendor/login">Vendor login</router-link></p>
<p class="consumer-link">Consumer? <router-link to="/signup">Sign up here</router-link></p>
</div>
</div>
</div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { APP_DISPLAY_NAME } from '@/constants/brand'
import { useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
import { useVendorAccount } from '@/composables/useVendorAccount'
import { isValidEmail, REGIONS, FOOD_CATEGORIES, CUISINE_TAGS, DIETARY_TAGS } from '@/utils/helpers'
import { loadPostalData, lookupPostal } from '@/utils/postalLookup'
import FormInput from '@/components/common/FormInput.vue'
import FormSelect from '@/components/common/FormSelect.vue'
import MultiSelectChips from '@/components/common/MultiSelectChips.vue'

const router = useRouter()
const { signup } = useAuth()
const { registerVendor } = useVendorAccount()

const form = reactive({
fullName: '', email: '', password: '', businessName: '', username: '',
contactNumber: '', locationText: '', region: '', lat: null, lng: null,
instagram: '', tiktok: '', lemon8: '',
categories: [], cuisineTags: [], dietaryTags: []
})

const errors = ref({})
const authError = ref('')
const submitting = ref(false)


const usePostalInput = ref(false)
const postalCode = ref('')
const postalError = ref('')
let postalDataCache = null

onMounted(async () => {
postalDataCache = await loadPostalData()
})

function togglePostalInput() {
usePostalInput.value = !usePostalInput.value
postalCode.value = ''
postalError.value = ''
form.lat = null
form.lng = null
}

function handlePostalLookup() {
postalError.value = ''
if (!postalDataCache) { postalError.value = 'Postal data not loaded yet'; return }
const result = lookupPostal(postalCode.value, postalDataCache)
if (result) {
form.lat = result.lat
form.lng = result.lng
} else {
postalError.value = 'Postal code not found'
}
}

async function handleRegister() {
errors.value = {}
authError.value = ''

if (!form.fullName.trim()) { errors.value.fullName = 'Required'; return }
if (!isValidEmail(form.email)) { errors.value.email = 'Valid email required'; return }
if (form.password.length < 6) { errors.value.password = 'Min 6 characters'; return }
if (!form.businessName.trim()) { errors.value.businessName = 'Required'; return }
if (!form.username.trim() || form.username.includes(' ')) { errors.value.username = 'Required, no spaces'; return }
if (!form.region) { errors.value.region = 'Required'; return }

submitting.value = true
try {
// Create Firebase Auth account as vendor
await signup({
email: form.email,
password: form.password,
fullName: form.fullName.trim(),
role: 'vendor'
})

// Create vendor document

await registerVendor({
businessName: form.businessName.trim(),
username: form.username.trim(),
Expand All @@ -107,7 +164,7 @@ async function handleRegister() {
cuisineTags: form.cuisineTags,
dietaryTags: form.dietaryTags
})

router.push('/vendor/dashboard')
} catch (e) {
if (e.code === 'auth/email-already-in-use') {
Expand All @@ -120,7 +177,7 @@ async function handleRegister() {
}
}
</script>

<style scoped>
.auth-page {
min-height: 100vh;
Expand All @@ -132,34 +189,39 @@ async function handleRegister() {
padding: var(--space-lg);
margin-top: -70px;
}

.auth-logo {
height: 90px;
width: auto;
margin-bottom: var(--space-md);
}

h2 {
text-align: center;
color: black;
margin-bottom: var(--space-xs);
}

.auth-subtitle {
text-align: center;
color: black;
margin-bottom: var(--space-lg);
}

.auth-card {
max-width: 520px;
width: 100%;
background: #F3E9DC;
}

.auth-error { text-align: center; margin-bottom: var(--space-md); }
.auth-links { margin-top: var(--space-lg); text-align: center; font-size: var(--font-size-sm); display: flex; flex-direction: column; gap: var(--space-sm); }
.consumer-link { color: var(--color-text-muted); }
.section-label { font-size: var(--font-size-base); color: var(--color-primary-dark); margin: var(--space-lg) 0 var(--space-sm); }
.coords-row { display: flex; gap: var(--space-sm); }
</style>
.coords-toggle { margin-bottom: var(--space-sm); }
.postal-row { display: flex; gap: var(--space-sm); align-items: center; }
.postal-row .form-input { flex: 1; }
.postal-success { font-size: var(--font-size-sm); color: #2a7a46; margin-top: 4px; }
</style>

Loading