| 1 | <template xmlns="http://www.w3.org/1999/html">
|
|---|
| 2 | <div class="profile-container">
|
|---|
| 3 | <!-- Header with back button -->
|
|---|
| 4 | <header class="header-section header-simple">
|
|---|
| 5 | <div class="container">
|
|---|
| 6 | <RouterLink to="/" class="back-link">← Back to listings</RouterLink>
|
|---|
| 7 | </div>
|
|---|
| 8 | </header>
|
|---|
| 9 |
|
|---|
| 10 | <!-- Loading State -->
|
|---|
| 11 | <div v-if="isLoading" class="container py-5 text-center text-muted">
|
|---|
| 12 | <p>Loading owner profile…</p>
|
|---|
| 13 | </div>
|
|---|
| 14 |
|
|---|
| 15 | <!-- Error State -->
|
|---|
| 16 | <div v-else-if="error" class="container py-5">
|
|---|
| 17 | <div class="alert alert-danger" role="alert">
|
|---|
| 18 | <div class="fw-semibold">Failed to load owner profile</div>
|
|---|
| 19 | <div class="small">{{ error }}</div>
|
|---|
| 20 | <button class="btn btn-sm btn-outline-danger mt-2" type="button" @click="reload">
|
|---|
| 21 | Try again
|
|---|
| 22 | </button>
|
|---|
| 23 | </div>
|
|---|
| 24 | </div>
|
|---|
| 25 |
|
|---|
| 26 | <!-- Owner Profile Content -->
|
|---|
| 27 | <div v-else-if="ownerInfo" class="profile-container">
|
|---|
| 28 | <!-- Owner Info Card -->
|
|---|
| 29 | <section class="header-section">
|
|---|
| 30 | <div class="container">
|
|---|
| 31 | <div class="profile-card">
|
|---|
| 32 | <div class="profile-content">
|
|---|
| 33 | <div class="profile-info">
|
|---|
| 34 | <div style="display: flex; align-items: center; gap: 12px;">
|
|---|
| 35 | <h1 class="profile-name">{{ ownerInfo.firstName }} {{ ownerInfo.lastName }}</h1>
|
|---|
| 36 | <span v-if="ownerInfo.verified" class="verified-badge">
|
|---|
| 37 | <img src="@/img/star.png" alt="verified" class="badge-star" /> Top 10
|
|---|
| 38 | </span>
|
|---|
| 39 | </div>
|
|---|
| 40 | <p class="profile-username">@{{ ownerInfo.username }}</p>
|
|---|
| 41 | <p class="profile-email">
|
|---|
| 42 | <i class="bi bi-envelope"></i>
|
|---|
| 43 | <a :href="`mailto:${ownerInfo.email}`">{{ ownerInfo.email }}</a>
|
|---|
| 44 | </p>
|
|---|
| 45 | </div>
|
|---|
| 46 | <div class="profile-badge">
|
|---|
| 47 | <button class="btn btn-primary btn-sm" type="button" @click="contactOwner">
|
|---|
| 48 | <i class="bi bi-envelope"></i> Contact Owner
|
|---|
| 49 | </button>
|
|---|
| 50 | </div>
|
|---|
| 51 | </div>
|
|---|
| 52 | </div>
|
|---|
| 53 | </div>
|
|---|
| 54 | </section>
|
|---|
| 55 |
|
|---|
| 56 | <!-- Tabs for listings and pets -->
|
|---|
| 57 | <section class="main-content">
|
|---|
| 58 | <div class="container">
|
|---|
| 59 | <div class="tabs-container">
|
|---|
| 60 | <ul class="nav nav-tabs nav-fill" role="tablist">
|
|---|
| 61 | <li class="nav-item" role="presentation">
|
|---|
| 62 | <button
|
|---|
| 63 | class="nav-link"
|
|---|
| 64 | :class="{ active: activeTab === 'listings' }"
|
|---|
| 65 | @click="activeTab = 'listings'"
|
|---|
| 66 | type="button"
|
|---|
| 67 | role="tab"
|
|---|
| 68 | >
|
|---|
| 69 | <i class="bi bi-bookmark-fill"></i> Active Listings ({{ activeOwnerListings.length }})
|
|---|
| 70 | </button>
|
|---|
| 71 | </li>
|
|---|
| 72 | <li class="nav-item" role="presentation">
|
|---|
| 73 | <button
|
|---|
| 74 | class="nav-link"
|
|---|
| 75 | :class="{ active: activeTab === 'pets' }"
|
|---|
| 76 | @click="activeTab = 'pets'"
|
|---|
| 77 | type="button"
|
|---|
| 78 | role="tab"
|
|---|
| 79 | >
|
|---|
| 80 | <i class="bi bi-paw-fill"></i> Pets ({{ ownerPets.length }})
|
|---|
| 81 | </button>
|
|---|
| 82 | </li>
|
|---|
| 83 | <li class="nav-item" role="presentation">
|
|---|
| 84 | <button
|
|---|
| 85 | class="nav-link"
|
|---|
| 86 | :class="{ active: activeTab === 'reviews' }"
|
|---|
| 87 | @click="activeTab = 'reviews'"
|
|---|
| 88 | type="button"
|
|---|
| 89 | role="tab"
|
|---|
| 90 | >
|
|---|
| 91 | <i class="bi bi-star-fill"></i> Reviews ({{ ownerReviews.length }})
|
|---|
| 92 | </button>
|
|---|
| 93 | </li>
|
|---|
| 94 | </ul>
|
|---|
| 95 |
|
|---|
| 96 | <!-- Listings Tab -->
|
|---|
| 97 | <div v-if="activeTab === 'listings'" class="tab-content-section">
|
|---|
| 98 | <h2 class="section-title">Active Listings</h2>
|
|---|
| 99 |
|
|---|
| 100 | <div v-if="activeOwnerListings.length === 0" class="empty-state">
|
|---|
| 101 | <p>This owner doesn't have active listings right now.</p>
|
|---|
| 102 | </div>
|
|---|
| 103 |
|
|---|
| 104 | <div v-else class="grid-container">
|
|---|
| 105 | <div v-for="listing in activeOwnerListings" :key="listing.listingId" class="listing-card-wrapper">
|
|---|
| 106 | <RouterLink :to="`/listing/${listing.listingId}`" class="listing-link">
|
|---|
| 107 | <div class="listing-card">
|
|---|
| 108 | <div class="listing-status" :class="statusClass(listing.status)">
|
|---|
| 109 | {{ listing.status || 'Active' }}
|
|---|
| 110 | </div>
|
|---|
| 111 |
|
|---|
| 112 | <h3 class="listing-title">{{ getPetName(listing.animalId) }}</h3>
|
|---|
| 113 | <p class="listing-description">{{ listing.description }}</p>
|
|---|
| 114 |
|
|---|
| 115 | <div class="listing-footer">
|
|---|
| 116 | <span class="listing-price" v-if="hasPrice(listing.price)">
|
|---|
| 117 | ${{ formatPrice(listing.price) }}
|
|---|
| 118 | </span>
|
|---|
| 119 | <span class="listing-date">
|
|---|
| 120 | {{ formatDate(listing.createdAt) }}
|
|---|
| 121 | </span>
|
|---|
| 122 | </div>
|
|---|
| 123 | </div>
|
|---|
| 124 | </RouterLink>
|
|---|
| 125 | </div>
|
|---|
| 126 | </div>
|
|---|
| 127 | </div>
|
|---|
| 128 |
|
|---|
| 129 | <!-- Pets Tab -->
|
|---|
| 130 | <div v-if="activeTab === 'pets'" class="tab-content-section">
|
|---|
| 131 | <h2 class="section-title">Owner's Pets</h2>
|
|---|
| 132 |
|
|---|
| 133 | <div v-if="ownerPets.length === 0" class="empty-state">
|
|---|
| 134 | <p>This owner hasn't added any pets yet.</p>
|
|---|
| 135 | </div>
|
|---|
| 136 |
|
|---|
| 137 | <div v-else class="grid-container">
|
|---|
| 138 | <div v-for="pet in ownerPets" :key="pet.animalId" class="pet-card-wrapper">
|
|---|
| 139 | <div class="pet-card">
|
|---|
| 140 | <div class="pet-image-wrapper">
|
|---|
| 141 | <img
|
|---|
| 142 | v-if="pet.photoUrl"
|
|---|
| 143 | :src="pet.photoUrl"
|
|---|
| 144 | :alt="pet.name"
|
|---|
| 145 | class="pet-image"
|
|---|
| 146 | @error="onPetImageError"
|
|---|
| 147 | />
|
|---|
| 148 | <img v-else src="@/img/all_outline.png" :alt="`${pet.name} placeholder`" class="pet-image-placeholder-img" />
|
|---|
| 149 | </div>
|
|---|
| 150 | <div class="pet-header">
|
|---|
| 151 | <h3 class="pet-name">{{ pet.name }}</h3>
|
|---|
| 152 | </div>
|
|---|
| 153 | <div class="pet-details">
|
|---|
| 154 | <div v-if="pet.species" class="pet-detail-row">
|
|---|
| 155 | <span class="label">Species</span>
|
|---|
| 156 | <span class="value">{{ pet.species }}</span>
|
|---|
| 157 | </div>
|
|---|
| 158 | <div v-if="pet.breed" class="pet-detail-row">
|
|---|
| 159 | <span class="label">Breed</span>
|
|---|
| 160 | <span class="value">{{ pet.breed }}</span>
|
|---|
| 161 | </div>
|
|---|
| 162 | <div v-if="pet.sex" class="pet-detail-row">
|
|---|
| 163 | <span class="label">Sex</span>
|
|---|
| 164 | <span class="value">{{ pet.sex }}</span>
|
|---|
| 165 | </div>
|
|---|
| 166 | <div v-if="pet.dateOfBirth" class="pet-detail-row">
|
|---|
| 167 | <span class="label">DOB</span>
|
|---|
| 168 | <span class="value">{{ formatDate(pet.dateOfBirth) }}</span>
|
|---|
| 169 | </div>
|
|---|
| 170 | </div>
|
|---|
| 171 | </div>
|
|---|
| 172 | </div>
|
|---|
| 173 | </div>
|
|---|
| 174 | </div>
|
|---|
| 175 |
|
|---|
| 176 | <!-- Reviews Tab -->
|
|---|
| 177 | <div v-if="activeTab === 'reviews'" class="tab-content-section">
|
|---|
| 178 | <h2 class="section-title">Reviews ({{ ownerReviews.length }})</h2>
|
|---|
| 179 |
|
|---|
| 180 | <!-- Add Review Form (only if logged in and not own profile) -->
|
|---|
| 181 | <div
|
|---|
| 182 | v-if="auth.isAuthenticated && ownerInfo && auth.user?.userId !== ownerInfo.userId"
|
|---|
| 183 | class="form-card"
|
|---|
| 184 | >
|
|---|
| 185 | <h3 class="section-title" style="font-size: 1.3rem; margin-top: 0">Leave a Review</h3>
|
|---|
| 186 | <form @submit.prevent="submitReview">
|
|---|
| 187 | <div class="form-group">
|
|---|
| 188 | <label class="form-label">Rating</label>
|
|---|
| 189 | <div class="rating-input">
|
|---|
| 190 | <button
|
|---|
| 191 | v-for="i in 5"
|
|---|
| 192 | :key="i"
|
|---|
| 193 | type="button"
|
|---|
| 194 | :class="['star-btn', { active: newReview.rating === i }]"
|
|---|
| 195 | @click="newReview.rating = i"
|
|---|
| 196 | >
|
|---|
| 197 | <img
|
|---|
| 198 | :src="starImg"
|
|---|
| 199 | :alt="`${i} star rating`"
|
|---|
| 200 | class="star-btn-img"
|
|---|
| 201 | :style="{ opacity: i <= newReview.rating ? 1 : 0.3 }"
|
|---|
| 202 | />
|
|---|
| 203 |
|
|---|
| 204 |
|
|---|
| 205 | </button>
|
|---|
| 206 | </div>
|
|---|
| 207 | </div>
|
|---|
| 208 |
|
|---|
| 209 | <div class="form-group">
|
|---|
| 210 | <label class="form-label" for="interactionType">How did you interact?</label>
|
|---|
| 211 | <select
|
|---|
| 212 | id="interactionType"
|
|---|
| 213 | v-model="newReview.interactionType"
|
|---|
| 214 | class="form-control"
|
|---|
| 215 | required
|
|---|
| 216 | >
|
|---|
| 217 | <option value="" disabled>Select an option</option>
|
|---|
| 218 | <option
|
|---|
| 219 | v-for="option in interactionTypeOptions"
|
|---|
| 220 | :key="option.value"
|
|---|
| 221 | :value="option.value"
|
|---|
| 222 | >
|
|---|
| 223 | {{ option.label }}
|
|---|
| 224 | </option>
|
|---|
| 225 | </select>
|
|---|
| 226 | </div>
|
|---|
| 227 |
|
|---|
| 228 | <div class="form-group">
|
|---|
| 229 | <label class="form-label" for="comment">Comment</label>
|
|---|
| 230 | <textarea
|
|---|
| 231 | id="comment"
|
|---|
| 232 | v-model="newReview.comment"
|
|---|
| 233 | class="form-control"
|
|---|
| 234 | placeholder="Share your experience with this owner..."
|
|---|
| 235 | rows="4"
|
|---|
| 236 | ></textarea>
|
|---|
| 237 | </div>
|
|---|
| 238 |
|
|---|
| 239 | <div v-if="reviewError" class="alert alert-danger" role="alert">
|
|---|
| 240 | {{ reviewError }}
|
|---|
| 241 | </div>
|
|---|
| 242 |
|
|---|
| 243 | <div class="form-actions">
|
|---|
| 244 | <button
|
|---|
| 245 | type="submit"
|
|---|
| 246 | class="btn btn-primary"
|
|---|
| 247 | :disabled="isSubmittingReview || newReview.rating === 0 || !newReview.interactionType"
|
|---|
| 248 | >
|
|---|
| 249 | <span v-if="isSubmittingReview">Submitting...</span>
|
|---|
| 250 | <span v-else>Submit Review</span>
|
|---|
| 251 | </button>
|
|---|
| 252 | </div>
|
|---|
| 253 | </form>
|
|---|
| 254 | </div>
|
|---|
| 255 |
|
|---|
| 256 | <!-- Reviews List -->
|
|---|
| 257 | <div v-if="ownerReviews.length === 0" class="empty-state">
|
|---|
| 258 | <p>No reviews yet. Be the first to leave a review!</p>
|
|---|
| 259 | </div>
|
|---|
| 260 |
|
|---|
| 261 | <div v-else class="reviews-list">
|
|---|
| 262 | <div v-for="review in ownerReviews" :key="review.reviewId" class="review-card">
|
|---|
| 263 | <div class="review-header">
|
|---|
| 264 | <div class="reviewer-info">
|
|---|
| 265 | <h4 class="reviewer-name">{{ review.reviewerName }}</h4>
|
|---|
| 266 | <p class="reviewer-username">@{{ review.reviewerUsername }}</p>
|
|---|
| 267 | </div>
|
|---|
| 268 | <div class="review-actions">
|
|---|
| 269 | <div class="rating">
|
|---|
| 270 | <img
|
|---|
| 271 | v-for="i in Number(review.rating || 0)"
|
|---|
| 272 | :key="i"
|
|---|
| 273 | src="@/img/star.png"
|
|---|
| 274 | alt="star"
|
|---|
| 275 | class="review-star"
|
|---|
| 276 | />
|
|---|
| 277 | </div>
|
|---|
| 278 | <button
|
|---|
| 279 | v-if="
|
|---|
| 280 | auth.isAuthenticated &&
|
|---|
| 281 | (auth.user?.userId === review.reviewerId || auth.user?.userId === ownerInfo?.userId)
|
|---|
| 282 | "
|
|---|
| 283 | type="button"
|
|---|
| 284 | class="delete-btn"
|
|---|
| 285 | @click="deleteReview(review.reviewId)"
|
|---|
| 286 | >
|
|---|
| 287 | <img src="@/img/trashcan.png" alt="delete" class="delete-btn-img" />
|
|---|
| 288 | </button>
|
|---|
| 289 | </div>
|
|---|
| 290 | </div>
|
|---|
| 291 | <p class="review-comment">{{ review.comment }}</p>
|
|---|
| 292 | <p class="review-date">{{ formatDate(review.createdAt) }}</p>
|
|---|
| 293 | </div>
|
|---|
| 294 | </div>
|
|---|
| 295 | </div>
|
|---|
| 296 | </div>
|
|---|
| 297 | </div>
|
|---|
| 298 | </section>
|
|---|
| 299 | </div>
|
|---|
| 300 | </div>
|
|---|
| 301 | </template>
|
|---|
| 302 |
|
|---|
| 303 | <script setup lang="ts">
|
|---|
| 304 | import starImg from '@/img/star.png'
|
|---|
| 305 | import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
|
|---|
| 306 | import { useRoute, RouterLink } from 'vue-router'
|
|---|
| 307 | import { getUserProfile, getUserListings, getUserPets, loadUserVerificationStatus } from '../api/profile'
|
|---|
| 308 | import {
|
|---|
| 309 | createReview,
|
|---|
| 310 | getReviewsByOwner,
|
|---|
| 311 | deleteReview as deleteReviewAPI,
|
|---|
| 312 | type UserReviewInteractionType,
|
|---|
| 313 | } from '../api/reviews'
|
|---|
| 314 | import { useAuthStore } from '../stores/auth'
|
|---|
| 315 |
|
|---|
| 316 | const route = useRoute()
|
|---|
| 317 | const auth = useAuthStore()
|
|---|
| 318 |
|
|---|
| 319 | const isLoading = ref(false)
|
|---|
| 320 | const error = ref<string | null>(null)
|
|---|
| 321 |
|
|---|
| 322 | const activeTab = ref<'listings' | 'pets' | 'reviews'>('listings')
|
|---|
| 323 |
|
|---|
| 324 | const ownerInfo = ref<any>(null)
|
|---|
| 325 | const ownerListings = ref<any[]>([])
|
|---|
| 326 | const ownerPets = ref<any[]>([])
|
|---|
| 327 | const ownerReviews = ref<any[]>([])
|
|---|
| 328 |
|
|---|
| 329 | const isSubmittingReview = ref(false)
|
|---|
| 330 | const reviewError = ref<string | null>(null)
|
|---|
| 331 | const interactionTypeOptions: Array<{ value: UserReviewInteractionType; label: string }> = [
|
|---|
| 332 | { value: 'EVENT', label: 'Met at an event' },
|
|---|
| 333 | { value: 'PERSONAL_INTERACTION', label: 'Personal interaction' },
|
|---|
| 334 | { value: 'ONLINE', label: 'Online interaction' },
|
|---|
| 335 | { value: 'PHONE_CALL', label: 'Phone call' },
|
|---|
| 336 | { value: 'OTHER', label: 'Other' },
|
|---|
| 337 | ]
|
|---|
| 338 |
|
|---|
| 339 | const newReview = ref<{
|
|---|
| 340 | rating: number
|
|---|
| 341 | comment: string
|
|---|
| 342 | interactionType: UserReviewInteractionType | ''
|
|---|
| 343 | }>({
|
|---|
| 344 | rating: 0,
|
|---|
| 345 | comment: '',
|
|---|
| 346 | interactionType: '',
|
|---|
| 347 | })
|
|---|
| 348 |
|
|---|
| 349 | // Create a map of petId to pet name
|
|---|
| 350 | const petNameMap = computed(() => {
|
|---|
| 351 | const map: Record<number, string> = {}
|
|---|
| 352 | ownerPets.value.forEach((pet) => {
|
|---|
| 353 | map[pet.animalId] = pet.name
|
|---|
| 354 | })
|
|---|
| 355 | return map
|
|---|
| 356 | })
|
|---|
| 357 |
|
|---|
| 358 | const activeOwnerListings = computed(() => {
|
|---|
| 359 | return ownerListings.value.filter((listing) => String(listing.status || 'ACTIVE').toUpperCase() === 'ACTIVE')
|
|---|
| 360 | })
|
|---|
| 361 |
|
|---|
| 362 | // Get pet name for listing
|
|---|
| 363 | function getPetName(animalId: number): string {
|
|---|
| 364 | return petNameMap.value[animalId] || 'Unknown Pet'
|
|---|
| 365 | }
|
|---|
| 366 |
|
|---|
| 367 | let abort: AbortController | null = null
|
|---|
| 368 |
|
|---|
| 369 | function extractArray(res: any): any[] {
|
|---|
| 370 | if (Array.isArray(res)) return res
|
|---|
| 371 | if (res?.content && Array.isArray(res.content)) return res.content
|
|---|
| 372 | if (res?.data && Array.isArray(res.data)) return res.data
|
|---|
| 373 | return []
|
|---|
| 374 | }
|
|---|
| 375 |
|
|---|
| 376 | async function load() {
|
|---|
| 377 | isLoading.value = true
|
|---|
| 378 | error.value = null
|
|---|
| 379 |
|
|---|
| 380 | ownerInfo.value = null
|
|---|
| 381 | ownerListings.value = []
|
|---|
| 382 | ownerPets.value = []
|
|---|
| 383 | ownerReviews.value = []
|
|---|
| 384 |
|
|---|
| 385 | abort?.abort()
|
|---|
| 386 | abort = new AbortController()
|
|---|
| 387 |
|
|---|
| 388 | try {
|
|---|
| 389 | const id = Number(route.params.ownerId)
|
|---|
| 390 | if (Number.isNaN(id)) throw new Error('Invalid owner ID')
|
|---|
| 391 |
|
|---|
| 392 | // NOTE: AbortController is kept to cancel UI state updates on route change;
|
|---|
| 393 | // if your API layer supports fetch signals, pass abort.signal inside those functions.
|
|---|
| 394 | const [userInfo, listingsRes, petsRes, reviewsRes] = await Promise.all([
|
|---|
| 395 | getUserProfile(id),
|
|---|
| 396 | getUserListings(id),
|
|---|
| 397 | getUserPets(id),
|
|---|
| 398 | getReviewsByOwner(id),
|
|---|
| 399 | ])
|
|---|
| 400 |
|
|---|
| 401 | ownerInfo.value = userInfo
|
|---|
| 402 | ownerListings.value = extractArray(listingsRes)
|
|---|
| 403 | ownerPets.value = extractArray(petsRes)
|
|---|
| 404 | ownerReviews.value = extractArray(reviewsRes)
|
|---|
| 405 |
|
|---|
| 406 | // Load verification status
|
|---|
| 407 | await loadOwnerVerification()
|
|---|
| 408 | } catch (e) {
|
|---|
| 409 | const message = e instanceof Error ? e.message : String(e)
|
|---|
| 410 | error.value = message
|
|---|
| 411 | console.error('Failed to load owner profile:', message)
|
|---|
| 412 | } finally {
|
|---|
| 413 | isLoading.value = false
|
|---|
| 414 | }
|
|---|
| 415 | }
|
|---|
| 416 |
|
|---|
| 417 | function reload() {
|
|---|
| 418 | load()
|
|---|
| 419 | }
|
|---|
| 420 |
|
|---|
| 421 | watch(
|
|---|
| 422 | () => route.params.ownerId,
|
|---|
| 423 | () => {
|
|---|
| 424 | // if user opens a different owner profile without leaving the page
|
|---|
| 425 | load()
|
|---|
| 426 | }
|
|---|
| 427 | )
|
|---|
| 428 |
|
|---|
| 429 | function onPetImageError(event: Event) {
|
|---|
| 430 | const img = event.target as HTMLImageElement
|
|---|
| 431 | img.style.display = 'none'
|
|---|
| 432 | }
|
|---|
| 433 |
|
|---|
| 434 | function formatDate(dateString: string): string {
|
|---|
| 435 | if (!dateString) return ''
|
|---|
| 436 | try {
|
|---|
| 437 | const date = new Date(dateString)
|
|---|
| 438 | if (Number.isNaN(date.getTime())) return ''
|
|---|
| 439 | return new Intl.DateTimeFormat(undefined, {
|
|---|
| 440 | month: 'short',
|
|---|
| 441 | day: 'numeric',
|
|---|
| 442 | year: 'numeric',
|
|---|
| 443 | }).format(date)
|
|---|
| 444 | } catch {
|
|---|
| 445 | return ''
|
|---|
| 446 | }
|
|---|
| 447 | }
|
|---|
| 448 |
|
|---|
| 449 | function hasPrice(price: unknown): boolean {
|
|---|
| 450 | const n = typeof price === 'number' ? price : Number(price)
|
|---|
| 451 | return Number.isFinite(n) && n > 0
|
|---|
| 452 | }
|
|---|
| 453 |
|
|---|
| 454 | function formatPrice(price: unknown): string {
|
|---|
| 455 | const n = typeof price === 'number' ? price : Number(price)
|
|---|
| 456 | if (!Number.isFinite(n)) return ''
|
|---|
| 457 | return n.toFixed(2)
|
|---|
| 458 | }
|
|---|
| 459 |
|
|---|
| 460 | function statusClass(status: unknown): string {
|
|---|
| 461 | const s = String(status ?? 'active').toLowerCase()
|
|---|
| 462 | return `status-${s}`
|
|---|
| 463 | }
|
|---|
| 464 |
|
|---|
| 465 | function contactOwner() {
|
|---|
| 466 | if (!ownerInfo.value?.email) {
|
|---|
| 467 | alert('Owner email not available')
|
|---|
| 468 | return
|
|---|
| 469 | }
|
|---|
| 470 | window.location.href = `mailto:${ownerInfo.value.email}`
|
|---|
| 471 | }
|
|---|
| 472 |
|
|---|
| 473 | async function submitReview() {
|
|---|
| 474 | if (!auth.isAuthenticated || !auth.user?.userId) {
|
|---|
| 475 | alert('Please log in to submit a review')
|
|---|
| 476 | return
|
|---|
| 477 | }
|
|---|
| 478 |
|
|---|
| 479 | if (newReview.value.rating === 0) {
|
|---|
| 480 | reviewError.value = 'Please select a rating'
|
|---|
| 481 | return
|
|---|
| 482 | }
|
|---|
| 483 |
|
|---|
| 484 | if (!newReview.value.interactionType) {
|
|---|
| 485 | reviewError.value = 'Please choose how you interacted with this user'
|
|---|
| 486 | return
|
|---|
| 487 | }
|
|---|
| 488 |
|
|---|
| 489 | isSubmittingReview.value = true
|
|---|
| 490 | reviewError.value = null
|
|---|
| 491 |
|
|---|
| 492 | try {
|
|---|
| 493 | await createReview(
|
|---|
| 494 | ownerInfo.value.userId,
|
|---|
| 495 | auth.user.userId,
|
|---|
| 496 | newReview.value.rating,
|
|---|
| 497 | newReview.value.comment,
|
|---|
| 498 | newReview.value.interactionType
|
|---|
| 499 | )
|
|---|
| 500 |
|
|---|
| 501 | // Reset form and reload reviews
|
|---|
| 502 | newReview.value.rating = 0
|
|---|
| 503 | newReview.value.comment = ''
|
|---|
| 504 | newReview.value.interactionType = ''
|
|---|
| 505 | await loadReviews()
|
|---|
| 506 | } catch (err) {
|
|---|
| 507 | reviewError.value = err instanceof Error ? err.message : 'Failed to submit review'
|
|---|
| 508 | } finally {
|
|---|
| 509 | isSubmittingReview.value = false
|
|---|
| 510 | }
|
|---|
| 511 | }
|
|---|
| 512 |
|
|---|
| 513 | async function loadReviews() {
|
|---|
| 514 | if (!ownerInfo.value?.userId) return
|
|---|
| 515 | try {
|
|---|
| 516 | const res = await getReviewsByOwner(ownerInfo.value.userId)
|
|---|
| 517 | ownerReviews.value = extractArray(res)
|
|---|
| 518 | } catch (err) {
|
|---|
| 519 | console.error('Failed to load reviews:', err)
|
|---|
| 520 | }
|
|---|
| 521 | }
|
|---|
| 522 |
|
|---|
| 523 | async function loadOwnerVerification() {
|
|---|
| 524 | if (!ownerInfo.value?.userId) return
|
|---|
| 525 |
|
|---|
| 526 | try {
|
|---|
| 527 | const isVerified = await loadUserVerificationStatus(ownerInfo.value.userId)
|
|---|
| 528 | if (ownerInfo.value) {
|
|---|
| 529 | ownerInfo.value.verified = isVerified
|
|---|
| 530 | }
|
|---|
| 531 | console.log(`✅ Owner verification status loaded: ${isVerified}`)
|
|---|
| 532 | } catch (error) {
|
|---|
| 533 | console.error('Failed to load owner verification status:', error)
|
|---|
| 534 | }
|
|---|
| 535 | }
|
|---|
| 536 |
|
|---|
| 537 | async function deleteReview(reviewId: number) {
|
|---|
| 538 | if (!auth.isAuthenticated || !auth.user?.userId) {
|
|---|
| 539 | alert('Please log in to delete a review')
|
|---|
| 540 | return
|
|---|
| 541 | }
|
|---|
| 542 |
|
|---|
| 543 | if (confirm('Are you sure you want to delete this review?')) {
|
|---|
| 544 | try {
|
|---|
| 545 | await deleteReviewAPI( reviewId, auth.user.userId)
|
|---|
| 546 | await loadReviews()
|
|---|
| 547 | } catch (err) {
|
|---|
| 548 | alert(err instanceof Error ? err.message : 'Failed to delete review')
|
|---|
| 549 | }
|
|---|
| 550 | }
|
|---|
| 551 | }
|
|---|
| 552 |
|
|---|
| 553 | onMounted(load)
|
|---|
| 554 | onBeforeUnmount(() => abort?.abort())
|
|---|
| 555 | </script>
|
|---|
| 556 |
|
|---|
| 557 | <style scoped>
|
|---|
| 558 | .profile-container {
|
|---|
| 559 | background: linear-gradient(135deg, #f5f7fa 0%, #f0f3f8 100%);
|
|---|
| 560 | min-height: 100vh;
|
|---|
| 561 | padding-bottom: 60px;
|
|---|
| 562 | }
|
|---|
| 563 |
|
|---|
| 564 | /* Header Section */
|
|---|
| 565 | .header-section {
|
|---|
| 566 | background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|---|
| 567 | padding: 40px 0;
|
|---|
| 568 | margin-bottom: 40px;
|
|---|
| 569 | box-shadow: 0 10px 30px rgba(249, 115, 22, 0.2);
|
|---|
| 570 | }
|
|---|
| 571 |
|
|---|
| 572 | .header-section.header-simple {
|
|---|
| 573 | background: white;
|
|---|
| 574 | padding: 20px 0;
|
|---|
| 575 | margin-bottom: 0;
|
|---|
| 576 | box-shadow: none;
|
|---|
| 577 | border-bottom: 1px solid #e2e8f0;
|
|---|
| 578 | }
|
|---|
| 579 |
|
|---|
| 580 | .back-link {
|
|---|
| 581 | display: inline-flex;
|
|---|
| 582 | align-items: center;
|
|---|
| 583 | color: #f97316;
|
|---|
| 584 | text-decoration: none;
|
|---|
| 585 | font-weight: 600;
|
|---|
| 586 | font-size: 0.95rem;
|
|---|
| 587 | transition: all 0.2s ease;
|
|---|
| 588 | }
|
|---|
| 589 |
|
|---|
| 590 | .back-link:hover {
|
|---|
| 591 | color: #ea580c;
|
|---|
| 592 | transform: translateX(-4px);
|
|---|
| 593 | }
|
|---|
| 594 |
|
|---|
| 595 | .profile-card {
|
|---|
| 596 | background: white;
|
|---|
| 597 | border-radius: 16px;
|
|---|
| 598 | padding: 30px;
|
|---|
| 599 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|---|
| 600 | }
|
|---|
| 601 |
|
|---|
| 602 | .profile-content {
|
|---|
| 603 | display: flex;
|
|---|
| 604 | justify-content: space-between;
|
|---|
| 605 | align-items: flex-start;
|
|---|
| 606 | gap: 30px;
|
|---|
| 607 | }
|
|---|
| 608 |
|
|---|
| 609 | .profile-info {
|
|---|
| 610 | flex: 1;
|
|---|
| 611 | }
|
|---|
| 612 |
|
|---|
| 613 | .profile-name {
|
|---|
| 614 | font-size: 2.5rem;
|
|---|
| 615 | font-weight: 700;
|
|---|
| 616 | color: #1a202c;
|
|---|
| 617 | margin: 0 0 12px 0;
|
|---|
| 618 | letter-spacing: -0.5px;
|
|---|
| 619 | }
|
|---|
| 620 |
|
|---|
| 621 | .profile-username {
|
|---|
| 622 | font-size: 1.1rem;
|
|---|
| 623 | color: #718096;
|
|---|
| 624 | margin: 0 0 8px 0;
|
|---|
| 625 | font-weight: 500;
|
|---|
| 626 | }
|
|---|
| 627 |
|
|---|
| 628 | .profile-email {
|
|---|
| 629 | font-size: 1rem;
|
|---|
| 630 | color: #4a5568;
|
|---|
| 631 | margin: 0;
|
|---|
| 632 | display: flex;
|
|---|
| 633 | align-items: center;
|
|---|
| 634 | gap: 8px;
|
|---|
| 635 | }
|
|---|
| 636 |
|
|---|
| 637 | .profile-email a {
|
|---|
| 638 | color: #4a5568;
|
|---|
| 639 | text-decoration: none;
|
|---|
| 640 | transition: color 0.2s ease;
|
|---|
| 641 | }
|
|---|
| 642 |
|
|---|
| 643 | .profile-email a:hover {
|
|---|
| 644 | color: #2d3748;
|
|---|
| 645 | }
|
|---|
| 646 |
|
|---|
| 647 | .profile-badge {
|
|---|
| 648 | display: flex;
|
|---|
| 649 | align-items: center;
|
|---|
| 650 | }
|
|---|
| 651 |
|
|---|
| 652 | .verified-badge {
|
|---|
| 653 | background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
|---|
| 654 | color: white;
|
|---|
| 655 | padding: 6px 14px;
|
|---|
| 656 | border-radius: 20px;
|
|---|
| 657 | font-size: 0.85rem;
|
|---|
| 658 | font-weight: 600;
|
|---|
| 659 | white-space: nowrap;
|
|---|
| 660 | display: flex;
|
|---|
| 661 | align-items: center;
|
|---|
| 662 | gap: 6px;
|
|---|
| 663 | box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
|
|---|
| 664 | }
|
|---|
| 665 |
|
|---|
| 666 | .badge-star {
|
|---|
| 667 | width: 18px;
|
|---|
| 668 | height: 18px;
|
|---|
| 669 | object-fit: contain;
|
|---|
| 670 | filter: brightness(0) invert(1);
|
|---|
| 671 | }
|
|---|
| 672 |
|
|---|
| 673 | /* Main Content */
|
|---|
| 674 | .main-content {
|
|---|
| 675 | padding: 0;
|
|---|
| 676 | }
|
|---|
| 677 |
|
|---|
| 678 | .tabs-container {
|
|---|
| 679 | background: white;
|
|---|
| 680 | border-radius: 12px;
|
|---|
| 681 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|---|
| 682 | overflow: hidden;
|
|---|
| 683 | }
|
|---|
| 684 |
|
|---|
| 685 | /* Tabs */
|
|---|
| 686 | .nav-tabs {
|
|---|
| 687 | border-bottom: 2px solid #e2e8f0;
|
|---|
| 688 | background: #f7fafc;
|
|---|
| 689 | padding: 0;
|
|---|
| 690 | margin: 0;
|
|---|
| 691 | }
|
|---|
| 692 |
|
|---|
| 693 | .nav-tabs .nav-link {
|
|---|
| 694 | color: #718096;
|
|---|
| 695 | border: none;
|
|---|
| 696 | border-bottom: 3px solid transparent;
|
|---|
| 697 | font-weight: 600;
|
|---|
| 698 | padding: 16px 24px;
|
|---|
| 699 | transition: all 0.3s ease;
|
|---|
| 700 | display: flex;
|
|---|
| 701 | align-items: center;
|
|---|
| 702 | gap: 8px;
|
|---|
| 703 | font-size: 0.95rem;
|
|---|
| 704 | }
|
|---|
| 705 |
|
|---|
| 706 | .nav-tabs .nav-link:hover {
|
|---|
| 707 | color: #2d3748;
|
|---|
| 708 | background: #edf2f7;
|
|---|
| 709 | }
|
|---|
| 710 |
|
|---|
| 711 | .nav-tabs .nav-link.active {
|
|---|
| 712 | color: #f97316;
|
|---|
| 713 | border-bottom-color: #f97316;
|
|---|
| 714 | background: white;
|
|---|
| 715 | }
|
|---|
| 716 |
|
|---|
| 717 | /* Tab Content */
|
|---|
| 718 | .tab-content-section {
|
|---|
| 719 | padding: 40px;
|
|---|
| 720 | animation: fadeIn 0.3s ease-in;
|
|---|
| 721 | }
|
|---|
| 722 |
|
|---|
| 723 | @keyframes fadeIn {
|
|---|
| 724 | from {
|
|---|
| 725 | opacity: 0;
|
|---|
| 726 | transform: translateY(10px);
|
|---|
| 727 | }
|
|---|
| 728 | to {
|
|---|
| 729 | opacity: 1;
|
|---|
| 730 | transform: translateY(0);
|
|---|
| 731 | }
|
|---|
| 732 | }
|
|---|
| 733 |
|
|---|
| 734 | .section-title {
|
|---|
| 735 | font-size: 1.8rem;
|
|---|
| 736 | font-weight: 700;
|
|---|
| 737 | color: #1a202c;
|
|---|
| 738 | margin: 0 0 30px 0;
|
|---|
| 739 | letter-spacing: -0.5px;
|
|---|
| 740 | }
|
|---|
| 741 |
|
|---|
| 742 | /* Empty State */
|
|---|
| 743 | .empty-state {
|
|---|
| 744 | text-align: center;
|
|---|
| 745 | padding: 60px 20px;
|
|---|
| 746 | color: #718096;
|
|---|
| 747 | }
|
|---|
| 748 |
|
|---|
| 749 | .empty-state p {
|
|---|
| 750 | font-size: 1.1rem;
|
|---|
| 751 | margin: 0;
|
|---|
| 752 | }
|
|---|
| 753 |
|
|---|
| 754 | /* Grid Container */
|
|---|
| 755 | .grid-container {
|
|---|
| 756 | display: grid;
|
|---|
| 757 | grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|---|
| 758 | gap: 24px;
|
|---|
| 759 | }
|
|---|
| 760 |
|
|---|
| 761 | /* Listing Card */
|
|---|
| 762 | .listing-card-wrapper {
|
|---|
| 763 | height: 100%;
|
|---|
| 764 | }
|
|---|
| 765 |
|
|---|
| 766 | .listing-card {
|
|---|
| 767 | background: white;
|
|---|
| 768 | border: none;
|
|---|
| 769 | border-radius: 16px;
|
|---|
| 770 | padding: 24px;
|
|---|
| 771 | height: 100%;
|
|---|
| 772 | display: flex;
|
|---|
| 773 | flex-direction: column;
|
|---|
| 774 | gap: 16px;
|
|---|
| 775 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|---|
| 776 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|---|
| 777 | position: relative;
|
|---|
| 778 | }
|
|---|
| 779 |
|
|---|
| 780 | .listing-card:hover {
|
|---|
| 781 | box-shadow: 0 16px 32px rgba(249, 115, 22, 0.12);
|
|---|
| 782 | transform: translateY(-8px);
|
|---|
| 783 | }
|
|---|
| 784 |
|
|---|
| 785 | .listing-link {
|
|---|
| 786 | text-decoration: none;
|
|---|
| 787 | display: block;
|
|---|
| 788 | height: 100%;
|
|---|
| 789 | }
|
|---|
| 790 |
|
|---|
| 791 | .listing-status {
|
|---|
| 792 | position: absolute;
|
|---|
| 793 | top: 16px;
|
|---|
| 794 | right: 16px;
|
|---|
| 795 | padding: 8px 14px;
|
|---|
| 796 | border-radius: 8px;
|
|---|
| 797 | font-size: 0.75rem;
|
|---|
| 798 | font-weight: 700;
|
|---|
| 799 | text-transform: uppercase;
|
|---|
| 800 | letter-spacing: 0.5px;
|
|---|
| 801 | }
|
|---|
| 802 |
|
|---|
| 803 | .status-active {
|
|---|
| 804 | background: #d1fae5;
|
|---|
| 805 | color: #065f46;
|
|---|
| 806 | }
|
|---|
| 807 |
|
|---|
| 808 | .status-pending {
|
|---|
| 809 | background: #fef3c7;
|
|---|
| 810 | color: #92400e;
|
|---|
| 811 | }
|
|---|
| 812 |
|
|---|
| 813 | .status-adopted {
|
|---|
| 814 | background: #dbeafe;
|
|---|
| 815 | color: #0c2d6b;
|
|---|
| 816 | }
|
|---|
| 817 |
|
|---|
| 818 | .listing-title {
|
|---|
| 819 | font-size: 1.3rem;
|
|---|
| 820 | font-weight: 700;
|
|---|
| 821 | color: #1a202c;
|
|---|
| 822 | margin: 0;
|
|---|
| 823 | line-height: 1.4;
|
|---|
| 824 | }
|
|---|
| 825 |
|
|---|
| 826 | .listing-description {
|
|---|
| 827 | color: #4a5568;
|
|---|
| 828 | font-size: 0.95rem;
|
|---|
| 829 | line-height: 1.6;
|
|---|
| 830 | margin: 0;
|
|---|
| 831 | flex: 1;
|
|---|
| 832 | overflow: hidden;
|
|---|
| 833 | text-overflow: ellipsis;
|
|---|
| 834 | display: -webkit-box;
|
|---|
| 835 | -webkit-line-clamp: 2;
|
|---|
| 836 | -webkit-box-orient: vertical;
|
|---|
| 837 | }
|
|---|
| 838 |
|
|---|
| 839 | .listing-footer {
|
|---|
| 840 | display: flex;
|
|---|
| 841 | justify-content: space-between;
|
|---|
| 842 | align-items: center;
|
|---|
| 843 | padding-top: 16px;
|
|---|
| 844 | margin-top: auto;
|
|---|
| 845 | }
|
|---|
| 846 |
|
|---|
| 847 | .listing-price {
|
|---|
| 848 | font-size: 1.5rem;
|
|---|
| 849 | font-weight: 800;
|
|---|
| 850 | color: #f97316;
|
|---|
| 851 | letter-spacing: -0.5px;
|
|---|
| 852 | }
|
|---|
| 853 |
|
|---|
| 854 | .listing-date {
|
|---|
| 855 | color: #a0aec0;
|
|---|
| 856 | font-size: 0.85rem;
|
|---|
| 857 | font-weight: 500;
|
|---|
| 858 | }
|
|---|
| 859 |
|
|---|
| 860 | .listing-actions {
|
|---|
| 861 | display: flex;
|
|---|
| 862 | gap: 8px;
|
|---|
| 863 | margin-top: auto;
|
|---|
| 864 | }
|
|---|
| 865 |
|
|---|
| 866 | .listing-actions .form-select {
|
|---|
| 867 | flex: 1;
|
|---|
| 868 | }
|
|---|
| 869 |
|
|---|
| 870 | /* Pet Card */
|
|---|
| 871 | .pet-card-wrapper {
|
|---|
| 872 | height: 100%;
|
|---|
| 873 | }
|
|---|
| 874 |
|
|---|
| 875 | .pet-card {
|
|---|
| 876 | background: white;
|
|---|
| 877 | border: none;
|
|---|
| 878 | border-radius: 16px;
|
|---|
| 879 | padding: 0;
|
|---|
| 880 | height: 100%;
|
|---|
| 881 | display: flex;
|
|---|
| 882 | flex-direction: column;
|
|---|
| 883 | gap: 16px;
|
|---|
| 884 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|---|
| 885 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|---|
| 886 | overflow: hidden;
|
|---|
| 887 | }
|
|---|
| 888 |
|
|---|
| 889 | .pet-card:hover {
|
|---|
| 890 | box-shadow: 0 16px 32px rgba(249, 115, 22, 0.12);
|
|---|
| 891 | transform: translateY(-8px);
|
|---|
| 892 | }
|
|---|
| 893 |
|
|---|
| 894 | .pet-image-wrapper {
|
|---|
| 895 | width: 100%;
|
|---|
| 896 | height: 220px;
|
|---|
| 897 | background: linear-gradient(135deg, #f5f7fa 0%, #f0f3f8 100%);
|
|---|
| 898 | display: flex;
|
|---|
| 899 | align-items: center;
|
|---|
| 900 | justify-content: center;
|
|---|
| 901 | overflow: hidden;
|
|---|
| 902 | }
|
|---|
| 903 |
|
|---|
| 904 | .pet-image {
|
|---|
| 905 | width: 100%;
|
|---|
| 906 | height: 100%;
|
|---|
| 907 | object-fit: cover;
|
|---|
| 908 | }
|
|---|
| 909 |
|
|---|
| 910 | .pet-image-placeholder {
|
|---|
| 911 | width: 100%;
|
|---|
| 912 | height: 100%;
|
|---|
| 913 | background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|---|
| 914 | display: flex;
|
|---|
| 915 | align-items: center;
|
|---|
| 916 | justify-content: center;
|
|---|
| 917 | }
|
|---|
| 918 |
|
|---|
| 919 | .pet-emoji {
|
|---|
| 920 | font-size: 3rem;
|
|---|
| 921 | }
|
|---|
| 922 |
|
|---|
| 923 | .pet-image-placeholder-img {
|
|---|
| 924 | width: 100%;
|
|---|
| 925 | height: 100%;
|
|---|
| 926 | object-fit: cover;
|
|---|
| 927 | }
|
|---|
| 928 |
|
|---|
| 929 | .pet-header {
|
|---|
| 930 | border-bottom: none;
|
|---|
| 931 | padding: 0 20px 0 20px;
|
|---|
| 932 | padding-top: 16px;
|
|---|
| 933 | }
|
|---|
| 934 |
|
|---|
| 935 | .pet-name {
|
|---|
| 936 | font-size: 1.3rem;
|
|---|
| 937 | font-weight: 700;
|
|---|
| 938 | margin: 0;
|
|---|
| 939 | color: #1a202c;
|
|---|
| 940 | line-height: 1.4;
|
|---|
| 941 | }
|
|---|
| 942 |
|
|---|
| 943 | .pet-details {
|
|---|
| 944 | list-style: none;
|
|---|
| 945 | padding: 0 20px 20px 20px;
|
|---|
| 946 | margin: 0;
|
|---|
| 947 | display: flex;
|
|---|
| 948 | flex-direction: column;
|
|---|
| 949 | gap: 12px;
|
|---|
| 950 | flex: 1;
|
|---|
| 951 | }
|
|---|
| 952 |
|
|---|
| 953 | .pet-detail-row {
|
|---|
| 954 | display: flex;
|
|---|
| 955 | justify-content: space-between;
|
|---|
| 956 | align-items: center;
|
|---|
| 957 | padding: 10px 0;
|
|---|
| 958 | border-bottom: none;
|
|---|
| 959 | font-size: 0.9rem;
|
|---|
| 960 | }
|
|---|
| 961 |
|
|---|
| 962 | .pet-detail-row:last-child {
|
|---|
| 963 | border-bottom: none;
|
|---|
| 964 | }
|
|---|
| 965 |
|
|---|
| 966 | .pet-detail-row .label {
|
|---|
| 967 | color: #718096;
|
|---|
| 968 | font-weight: 600;
|
|---|
| 969 | text-transform: capitalize;
|
|---|
| 970 | font-size: 0.9rem;
|
|---|
| 971 | }
|
|---|
| 972 |
|
|---|
| 973 | .pet-detail-row .value {
|
|---|
| 974 | color: #2d3748;
|
|---|
| 975 | font-weight: 500;
|
|---|
| 976 | }
|
|---|
| 977 |
|
|---|
| 978 | /* Review Card */
|
|---|
| 979 | .review-card {
|
|---|
| 980 | background: white;
|
|---|
| 981 | border: none;
|
|---|
| 982 | border-radius: 16px;
|
|---|
| 983 | padding: 24px;
|
|---|
| 984 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|---|
| 985 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|---|
| 986 | }
|
|---|
| 987 |
|
|---|
| 988 | .review-card:hover {
|
|---|
| 989 | box-shadow: 0 12px 24px rgba(249, 115, 22, 0.1);
|
|---|
| 990 | transform: translateY(-4px);
|
|---|
| 991 | }
|
|---|
| 992 |
|
|---|
| 993 | /* Form Card */
|
|---|
| 994 | .form-card {
|
|---|
| 995 | background: white;
|
|---|
| 996 | border: none;
|
|---|
| 997 | border-radius: 16px;
|
|---|
| 998 | padding: 32px;
|
|---|
| 999 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|---|
| 1000 | margin-bottom: 32px;
|
|---|
| 1001 | }
|
|---|
| 1002 |
|
|---|
| 1003 | .form-group {
|
|---|
| 1004 | margin-bottom: 20px;
|
|---|
| 1005 | }
|
|---|
| 1006 |
|
|---|
| 1007 | .form-label {
|
|---|
| 1008 | display: block;
|
|---|
| 1009 | font-weight: 600;
|
|---|
| 1010 | color: #2d3748;
|
|---|
| 1011 | margin-bottom: 8px;
|
|---|
| 1012 | font-size: 0.95rem;
|
|---|
| 1013 | }
|
|---|
| 1014 |
|
|---|
| 1015 | .rating-input {
|
|---|
| 1016 | display: flex;
|
|---|
| 1017 | gap: 8px;
|
|---|
| 1018 | flex-wrap: wrap;
|
|---|
| 1019 | }
|
|---|
| 1020 |
|
|---|
| 1021 | .star-btn {
|
|---|
| 1022 | background: white;
|
|---|
| 1023 | border: 2px solid #e2e8f0;
|
|---|
| 1024 | border-radius: 8px;
|
|---|
| 1025 | padding: 10px 14px;
|
|---|
| 1026 | font-size: 1.5rem;
|
|---|
| 1027 | cursor: pointer;
|
|---|
| 1028 | transition: all 0.2s ease;
|
|---|
| 1029 | display: flex;
|
|---|
| 1030 | align-items: center;
|
|---|
| 1031 | justify-content: center;
|
|---|
| 1032 | }
|
|---|
| 1033 |
|
|---|
| 1034 | .star-btn:hover {
|
|---|
| 1035 | border-color: #f97316;
|
|---|
| 1036 | background: #fff8f1;
|
|---|
| 1037 | }
|
|---|
| 1038 |
|
|---|
| 1039 | .star-btn.active {
|
|---|
| 1040 | border-color: #f97316;
|
|---|
| 1041 | background: #fff8f1;
|
|---|
| 1042 | }
|
|---|
| 1043 |
|
|---|
| 1044 | .star-btn-img {
|
|---|
| 1045 | width: 1.5rem;
|
|---|
| 1046 | height: 1.5rem;
|
|---|
| 1047 | object-fit: contain;
|
|---|
| 1048 | }
|
|---|
| 1049 |
|
|---|
| 1050 | .form-control {
|
|---|
| 1051 | width: 100%;
|
|---|
| 1052 | padding: 12px;
|
|---|
| 1053 | border: 1.5px solid #e2e8f0;
|
|---|
| 1054 | border-radius: 8px;
|
|---|
| 1055 | font-family: inherit;
|
|---|
| 1056 | font-size: 0.95rem;
|
|---|
| 1057 | color: #1a202c;
|
|---|
| 1058 | resize: vertical;
|
|---|
| 1059 | transition: all 0.2s ease;
|
|---|
| 1060 | }
|
|---|
| 1061 |
|
|---|
| 1062 | .form-control:focus {
|
|---|
| 1063 | outline: none;
|
|---|
| 1064 | border-color: #f97316;
|
|---|
| 1065 | box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.1);
|
|---|
| 1066 | }
|
|---|
| 1067 |
|
|---|
| 1068 | .form-actions {
|
|---|
| 1069 | display: flex;
|
|---|
| 1070 | gap: 12px;
|
|---|
| 1071 | margin-top: 24px;
|
|---|
| 1072 | padding-top: 24px;
|
|---|
| 1073 | border-top: 1px solid #e2e8f0;
|
|---|
| 1074 | }
|
|---|
| 1075 |
|
|---|
| 1076 | .reviews-list {
|
|---|
| 1077 | display: flex;
|
|---|
| 1078 | flex-direction: column;
|
|---|
| 1079 | gap: 20px;
|
|---|
| 1080 | }
|
|---|
| 1081 |
|
|---|
| 1082 | /* Buttons */
|
|---|
| 1083 | .btn {
|
|---|
| 1084 | border-radius: 8px;
|
|---|
| 1085 | font-weight: 600;
|
|---|
| 1086 | padding: 10px 20px;
|
|---|
| 1087 | transition: all 0.2s ease;
|
|---|
| 1088 | font-size: 0.95rem;
|
|---|
| 1089 | border: none;
|
|---|
| 1090 | cursor: pointer;
|
|---|
| 1091 | }
|
|---|
| 1092 |
|
|---|
| 1093 | .btn-primary {
|
|---|
| 1094 | background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
|---|
| 1095 | color: white;
|
|---|
| 1096 | }
|
|---|
| 1097 |
|
|---|
| 1098 | .btn-primary:hover:not(:disabled) {
|
|---|
| 1099 | transform: translateY(-2px);
|
|---|
| 1100 | box-shadow: 0 8px 20px rgba(249, 115, 22, 0.3);
|
|---|
| 1101 | }
|
|---|
| 1102 |
|
|---|
| 1103 | .btn-sm {
|
|---|
| 1104 | padding: 6px 12px;
|
|---|
| 1105 | font-size: 0.85rem;
|
|---|
| 1106 | }
|
|---|
| 1107 |
|
|---|
| 1108 | .btn:disabled {
|
|---|
| 1109 | opacity: 0.5;
|
|---|
| 1110 | cursor: not-allowed;
|
|---|
| 1111 | }
|
|---|
| 1112 |
|
|---|
| 1113 | .btn-outline-danger {
|
|---|
| 1114 | border: 1.5px solid #f56565;
|
|---|
| 1115 | color: #f56565;
|
|---|
| 1116 | background: transparent;
|
|---|
| 1117 | }
|
|---|
| 1118 |
|
|---|
| 1119 | .btn-outline-danger:hover {
|
|---|
| 1120 | background: #fff5f5;
|
|---|
| 1121 | border-color: #e53e3e;
|
|---|
| 1122 | }
|
|---|
| 1123 |
|
|---|
| 1124 | /* Alerts */
|
|---|
| 1125 | .alert {
|
|---|
| 1126 | padding: 16px;
|
|---|
| 1127 | border-radius: 8px;
|
|---|
| 1128 | margin-bottom: 16px;
|
|---|
| 1129 | }
|
|---|
| 1130 |
|
|---|
| 1131 | .alert-danger {
|
|---|
| 1132 | background: #fee2e2;
|
|---|
| 1133 | color: #991b1b;
|
|---|
| 1134 | border: 1px solid #fecaca;
|
|---|
| 1135 | }
|
|---|
| 1136 |
|
|---|
| 1137 |
|
|---|
| 1138 | .review-header {
|
|---|
| 1139 | display: flex;
|
|---|
| 1140 | justify-content: space-between;
|
|---|
| 1141 | align-items: flex-start;
|
|---|
| 1142 | margin-bottom: 16px;
|
|---|
| 1143 | flex-wrap: wrap;
|
|---|
| 1144 | gap: 12px;
|
|---|
| 1145 | }
|
|---|
| 1146 |
|
|---|
| 1147 | .reviewer-info {
|
|---|
| 1148 | flex: 1;
|
|---|
| 1149 | }
|
|---|
| 1150 |
|
|---|
| 1151 | .reviewer-name {
|
|---|
| 1152 | font-size: 1rem;
|
|---|
| 1153 | font-weight: 700;
|
|---|
| 1154 | margin: 0 0 4px 0;
|
|---|
| 1155 | color: #111827;
|
|---|
| 1156 | }
|
|---|
| 1157 |
|
|---|
| 1158 | .reviewer-username {
|
|---|
| 1159 | font-size: 0.875rem;
|
|---|
| 1160 | color: #6b7280;
|
|---|
| 1161 | margin: 0;
|
|---|
| 1162 | }
|
|---|
| 1163 |
|
|---|
| 1164 | .review-actions {
|
|---|
| 1165 | display: flex;
|
|---|
| 1166 | align-items: center;
|
|---|
| 1167 | gap: 12px;
|
|---|
| 1168 | }
|
|---|
| 1169 |
|
|---|
| 1170 | .rating {
|
|---|
| 1171 | font-size: 1rem;
|
|---|
| 1172 | color: #f59e0b;
|
|---|
| 1173 | display: flex;
|
|---|
| 1174 | gap: 6px;
|
|---|
| 1175 | align-items: center;
|
|---|
| 1176 | }
|
|---|
| 1177 |
|
|---|
| 1178 | .review-star {
|
|---|
| 1179 | width: 1.5rem;
|
|---|
| 1180 | height: 1.5rem;
|
|---|
| 1181 | object-fit: contain;
|
|---|
| 1182 | }
|
|---|
| 1183 |
|
|---|
| 1184 | .delete-btn {
|
|---|
| 1185 | background: none;
|
|---|
| 1186 | border: none;
|
|---|
| 1187 | cursor: pointer;
|
|---|
| 1188 | opacity: 0.6;
|
|---|
| 1189 | transition: opacity 0.2s ease;
|
|---|
| 1190 | padding: 4px 8px;
|
|---|
| 1191 | display: flex;
|
|---|
| 1192 | align-items: center;
|
|---|
| 1193 | justify-content: center;
|
|---|
| 1194 | }
|
|---|
| 1195 |
|
|---|
| 1196 | .delete-btn:hover {
|
|---|
| 1197 | opacity: 1;
|
|---|
| 1198 | }
|
|---|
| 1199 |
|
|---|
| 1200 | .delete-btn-img {
|
|---|
| 1201 | width: 1.25rem;
|
|---|
| 1202 | height: 1.25rem;
|
|---|
| 1203 | object-fit: contain;
|
|---|
| 1204 | }
|
|---|
| 1205 |
|
|---|
| 1206 | .review-comment {
|
|---|
| 1207 | color: #374151;
|
|---|
| 1208 | line-height: 1.6;
|
|---|
| 1209 | margin: 0 0 12px 0;
|
|---|
| 1210 | white-space: pre-wrap;
|
|---|
| 1211 | word-break: break-word;
|
|---|
| 1212 | }
|
|---|
| 1213 |
|
|---|
| 1214 | .review-date {
|
|---|
| 1215 | font-size: 0.875rem;
|
|---|
| 1216 | color: #9ca3af;
|
|---|
| 1217 | margin: 0;
|
|---|
| 1218 | }
|
|---|
| 1219 |
|
|---|
| 1220 | /* Responsive */
|
|---|
| 1221 | @media (max-width: 768px) {
|
|---|
| 1222 | .owner-card {
|
|---|
| 1223 | padding: 24px;
|
|---|
| 1224 | }
|
|---|
| 1225 |
|
|---|
| 1226 | .owner-header {
|
|---|
| 1227 | flex-direction: column;
|
|---|
| 1228 | align-items: flex-start;
|
|---|
| 1229 | }
|
|---|
| 1230 |
|
|---|
| 1231 | .owner-name {
|
|---|
| 1232 | font-size: 1.75rem;
|
|---|
| 1233 | }
|
|---|
| 1234 |
|
|---|
| 1235 | .contact-actions {
|
|---|
| 1236 | width: 100%;
|
|---|
| 1237 | }
|
|---|
| 1238 |
|
|---|
| 1239 | .btn-contact {
|
|---|
| 1240 | flex: 1;
|
|---|
| 1241 | }
|
|---|
| 1242 |
|
|---|
| 1243 | .tab-content {
|
|---|
| 1244 | padding: 24px;
|
|---|
| 1245 | }
|
|---|
| 1246 |
|
|---|
| 1247 | .listings-grid,
|
|---|
| 1248 | .pets-grid {
|
|---|
| 1249 | grid-template-columns: 1fr;
|
|---|
| 1250 | }
|
|---|
| 1251 | }
|
|---|
| 1252 |
|
|---|
| 1253 | @media (max-width: 576px) {
|
|---|
| 1254 | .header-section {
|
|---|
| 1255 | padding: 12px 0;
|
|---|
| 1256 | }
|
|---|
| 1257 |
|
|---|
| 1258 | .back-link {
|
|---|
| 1259 | font-size: 0.875rem;
|
|---|
| 1260 | }
|
|---|
| 1261 |
|
|---|
| 1262 | .owner-card {
|
|---|
| 1263 | padding: 16px;
|
|---|
| 1264 | }
|
|---|
| 1265 |
|
|---|
| 1266 | .owner-name {
|
|---|
| 1267 | font-size: 1.5rem;
|
|---|
| 1268 | }
|
|---|
| 1269 |
|
|---|
| 1270 | .owner-username,
|
|---|
| 1271 | .owner-email {
|
|---|
| 1272 | font-size: 0.875rem;
|
|---|
| 1273 | }
|
|---|
| 1274 |
|
|---|
| 1275 | .tabs-header {
|
|---|
| 1276 | flex-direction: column;
|
|---|
| 1277 | }
|
|---|
| 1278 |
|
|---|
| 1279 | .tab-button {
|
|---|
| 1280 | padding: 16px;
|
|---|
| 1281 | border-right: 3px solid transparent;
|
|---|
| 1282 | border-bottom: none;
|
|---|
| 1283 | }
|
|---|
| 1284 |
|
|---|
| 1285 | .tab-button.active {
|
|---|
| 1286 | border-right-color: #d97706;
|
|---|
| 1287 | border-bottom-color: transparent;
|
|---|
| 1288 | }
|
|---|
| 1289 |
|
|---|
| 1290 | .tab-content {
|
|---|
| 1291 | padding: 16px;
|
|---|
| 1292 | }
|
|---|
| 1293 |
|
|---|
| 1294 | .section-title {
|
|---|
| 1295 | font-size: 1.25rem;
|
|---|
| 1296 | }
|
|---|
| 1297 | }
|
|---|
| 1298 | </style>
|
|---|