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