also looking at this
Migrate :text bindings to {{ }} interpolation (stx 0.2.72)
#67
52 files
+344
-434
| @@ -38,12 +38,12 @@ | ||
| 38 | 38 | <div class="gap-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> |
| 39 | 39 | <StxLink to="/admin/reviews" class="block p-6 bg-white border border-gray-200 rounded-lg hover:shadow-sm transition-shadow"> |
| 40 | 40 | <p class="font-medium text-gray-500 text-sm">Pending reviews</p> |
| 41 | <p class="mt-2 font-bold text-3xl text-gray-900" :text="pendingCount()"></p> | |
| 41 | <p class="mt-2 font-bold text-3xl text-gray-900">{{ pendingCount() }}</p> | |
| 42 | 42 | <p class="mt-2 font-semibold text-gray-600 text-sm">Open moderation queue →</p> |
| 43 | 43 | </StxLink> |
| 44 | 44 | <StxLink to="/admin/users" class="block p-6 bg-white border border-gray-200 rounded-lg hover:shadow-sm transition-shadow"> |
| 45 | 45 | <p class="font-medium text-gray-500 text-sm">Total users</p> |
| 46 | <p class="mt-2 font-bold text-3xl text-gray-900" :text="userCount()"></p> | |
| 46 | <p class="mt-2 font-bold text-3xl text-gray-900">{{ userCount() }}</p> | |
| 47 | 47 | <p class="mt-2 font-semibold text-gray-600 text-sm">Manage users →</p> |
| 48 | 48 | </StxLink> |
| 49 | 49 | <div class="block p-6 bg-white border border-gray-200 rounded-lg"> |
| @@ -107,7 +107,7 @@ | ||
| 107 | 107 | </nav> |
| 108 | 108 | </div> |
| 109 | 109 | <div class="flex gap-x-4 items-center"> |
| 110 | <span class="hidden sm:inline text-gray-600 text-sm" :text="adminName"></span> | |
| 110 | <span class="hidden sm:inline text-gray-600 text-sm">{{ adminName }}</span> | |
| 111 | 111 | <StxLink to="/" class="text-gray-500 text-sm hover:text-gray-900">View site</StxLink> |
| 112 | 112 | <button |
| 113 | 113 | type="button" |
| @@ -75,9 +75,7 @@ | ||
| 75 | 75 | <div |
| 76 | 76 | role="alert" |
| 77 | 77 | class="px-4 py-3 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 78 | x-class="store.actionError() ? '' : 'hidden'" | |
| 79 | :text="store.actionError()" | |
| 80 | ></div> | |
| 78 | x-class="store.actionError() ? '' : 'hidden'">{{ store.actionError() }}</div> | |
| 81 | 79 | |
| 82 | 80 | <div class="bg-white border border-gray-200 rounded-lg overflow-hidden"> |
| 83 | 81 | <div :if="store.loading() && store.claims().length === 0" class="px-4 py-12 text-center text-gray-500 text-sm"> |
| @@ -100,23 +98,21 @@ | ||
| 100 | 98 | <div class="flex flex-wrap gap-2 items-center"> |
| 101 | 99 | <span |
| 102 | 100 | class="inline-block px-2 py-0.5 font-medium text-xs rounded-full" |
| 103 | x-class="isJudgeClaim(claim) ? 'bg-amber-100 text-amber-800' : 'bg-gray-100 text-gray-700'" | |
| 104 | :text="isJudgeClaim(claim) ? 'Judge claim' : 'Credential'" | |
| 105 | ></span> | |
| 106 | <span class="text-gray-500 text-xs" :text="formatDate(claim.credential_claimed_at)"></span> | |
| 101 | x-class="isJudgeClaim(claim) ? 'bg-amber-100 text-amber-800' : 'bg-gray-100 text-gray-700'">{{ isJudgeClaim(claim) ? 'Judge claim' : 'Credential' }}</span> | |
| 102 | <span class="text-gray-500 text-xs">{{ formatDate(claim.credential_claimed_at) }}</span> | |
| 107 | 103 | </div> |
| 108 | <h3 class="mt-1 font-semibold text-gray-900 text-base" :text="claimLabel(claim)"></h3> | |
| 104 | <h3 class="mt-1 font-semibold text-gray-900 text-base">{{ claimLabel(claim) }}</h3> | |
| 109 | 105 | <p class="mt-0.5 text-gray-600 text-sm"> |
| 110 | <span :text="claim.name"></span> | |
| 106 | <span>{{ claim.name }}</span> | |
| 111 | 107 | <span class="text-gray-400"> · </span> |
| 112 | <span :text="claim.email"></span> | |
| 108 | <span>{{ claim.email }}</span> | |
| 113 | 109 | </p> |
| 114 | 110 | <!-- Prior rejection note, if this claim was rejected and resubmitted. --> |
| 115 | 111 | <p |
| 116 | 112 | class="mt-2 px-2 py-1 text-amber-800 text-xs bg-amber-50 border border-amber-200 rounded" |
| 117 | 113 | x-class="hasNote(claim) ? '' : 'hidden'" |
| 118 | 114 | > |
| 119 | Previously rejected: <span :text="claim.credential_rejection_note"></span> | |
| 115 | Previously rejected: <span>{{ claim.credential_rejection_note }}</span> | |
| 120 | 116 | </p> |
| 121 | 117 | </div> |
| 122 | 118 | <div class="flex flex-shrink-0 gap-x-2 items-start"> |
| @@ -56,9 +56,7 @@ | ||
| 56 | 56 | <div |
| 57 | 57 | role="alert" |
| 58 | 58 | class="px-4 py-3 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 59 | x-class="store.error() ? '' : 'hidden'" | |
| 60 | :text="store.error()" | |
| 61 | ></div> | |
| 59 | x-class="store.error() ? '' : 'hidden'">{{ store.error() }}</div> | |
| 62 | 60 | |
| 63 | 61 | <div class="bg-white border border-gray-200 rounded-lg overflow-hidden"> |
| 64 | 62 | <div :if="store.loading() && store.logs().length === 0" class="px-4 py-12 text-center text-gray-500 text-sm"> |
| @@ -76,14 +74,14 @@ | ||
| 76 | 74 | <ul x-class="hasLogs() ? 'divide-gray-100 divide-y' : 'hidden'"> |
| 77 | 75 | <li :for="entry in store.logs" class="px-4 py-3"> |
| 78 | 76 | <div class="flex flex-wrap gap-2 items-center"> |
| 79 | <span class="inline-block px-2 py-0.5 font-medium text-xs rounded-full" x-class="actionTone(entry)" :text="actionLabel(entry)"></span> | |
| 80 | <span class="font-medium text-gray-900 text-sm" :text="targetLabel(entry)"></span> | |
| 77 | <span class="inline-block px-2 py-0.5 font-medium text-xs rounded-full" x-class="actionTone(entry)">{{ actionLabel(entry) }}</span> | |
| 78 | <span class="font-medium text-gray-900 text-sm">{{ targetLabel(entry) }}</span> | |
| 81 | 79 | <span class="text-gray-400 text-xs">·</span> |
| 82 | <span class="text-gray-600 text-xs">by <span :text="entry.actor_name"></span></span> | |
| 80 | <span class="text-gray-600 text-xs">by <span>{{ entry.actor_name }}</span></span> | |
| 83 | 81 | <span class="text-gray-400 text-xs">·</span> |
| 84 | <span class="text-gray-500 text-xs" :text="formatDate(entry.created_at)"></span> | |
| 82 | <span class="text-gray-500 text-xs">{{ formatDate(entry.created_at) }}</span> | |
| 85 | 83 | </div> |
| 86 | <p class="mt-1 text-gray-600 text-xs" x-class="hasNote(entry) ? '' : 'hidden'" :text="entry.note"></p> | |
| 84 | <p class="mt-1 text-gray-600 text-xs" x-class="hasNote(entry) ? '' : 'hidden'">{{ entry.note }}</p> | |
| 87 | 85 | </li> |
| 88 | 86 | </ul> |
| 89 | 87 | </div> |
| @@ -156,7 +156,7 @@ | ||
| 156 | 156 | <div> |
| 157 | 157 | <h1 class="font-bold text-2xl text-gray-900">Reviews</h1> |
| 158 | 158 | <p class="mt-1 text-gray-600 text-sm"> |
| 159 | <span :text="store.total()"></span> <span :text="store.statusFilter()"></span> | |
| 159 | <span>{{ store.total() }}</span> <span>{{ store.statusFilter() }}</span> | |
| 160 | 160 | </p> |
| 161 | 161 | </div> |
| 162 | 162 | <div> |
| @@ -177,17 +177,13 @@ | ||
| 177 | 177 | type="button" |
| 178 | 178 | @click="setFilter(tab.key)" |
| 179 | 179 | class="px-4 py-2 font-medium text-sm border-b-2 transition-colors" |
| 180 | x-class="store.statusFilter() === tab.key ? 'border-gray-900 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-900 hover:border-gray-300'" | |
| 181 | :text="tab.label" | |
| 182 | ></button> | |
| 180 | x-class="store.statusFilter() === tab.key ? 'border-gray-900 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-900 hover:border-gray-300'">{{ tab.label }}</button> | |
| 183 | 181 | </div> |
| 184 | 182 | |
| 185 | 183 | <div |
| 186 | 184 | role="alert" |
| 187 | 185 | class="px-4 py-3 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 188 | x-class="store.actionError() ? '' : 'hidden'" | |
| 189 | :text="store.actionError()" | |
| 190 | ></div> | |
| 186 | x-class="store.actionError() ? '' : 'hidden'">{{ store.actionError() }}</div> | |
| 191 | 187 | |
| 192 | 188 | <div class="bg-white border border-gray-200 rounded-lg overflow-hidden"> |
| 193 | 189 | <div :if="store.loading() && store.reviews().length === 0" class="px-4 py-12 text-center text-gray-500 text-sm"> |
| @@ -206,8 +202,8 @@ | ||
| 206 | 202 | <path d="M9 12h6m-6 4h6M7 4h10a2 2 0 012 2v12a2 2 0 01-2 2H7a2 2 0 01-2-2V6a2 2 0 012-2z" /> |
| 207 | 203 | </svg> |
| 208 | 204 | </div> |
| 209 | <p class="font-semibold text-gray-900 text-sm" :text="emptyTitle()"></p> | |
| 210 | <p class="mt-1 max-w-md mx-auto text-gray-500 text-xs leading-relaxed" :text="emptyHint()"></p> | |
| 205 | <p class="font-semibold text-gray-900 text-sm">{{ emptyTitle() }}</p> | |
| 206 | <p class="mt-1 max-w-md mx-auto text-gray-500 text-xs leading-relaxed">{{ emptyHint() }}</p> | |
| 211 | 207 | </div> |
| 212 | 208 | <!-- x-class hidden (not :if) so the inner :for keeps its |
| 213 | 209 | subscription across the loading -> loaded transition. The |
| @@ -220,19 +216,17 @@ | ||
| 220 | 216 | <div class="flex gap-x-2 items-center"> |
| 221 | 217 | <span |
| 222 | 218 | class="inline-block px-2 py-0.5 font-medium text-xs rounded-full" |
| 223 | x-class="review.status === 'published' ? 'bg-emerald-100 text-emerald-800' : review.status === 'rejected' ? 'bg-red-100 text-red-800' : 'bg-amber-100 text-amber-800'" | |
| 224 | :text="review.status" | |
| 225 | ></span> | |
| 226 | <span class="text-gray-500 text-xs" :text="formatDate(review.created_at)"></span> | |
| 219 | x-class="review.status === 'published' ? 'bg-emerald-100 text-emerald-800' : review.status === 'rejected' ? 'bg-red-100 text-red-800' : 'bg-amber-100 text-amber-800'">{{ review.status }}</span> | |
| 220 | <span class="text-gray-500 text-xs">{{ formatDate(review.created_at) }}</span> | |
| 227 | 221 | </div> |
| 228 | <h3 class="mt-1 font-semibold text-gray-900 text-base" :text="review.title"></h3> | |
| 222 | <h3 class="mt-1 font-semibold text-gray-900 text-base">{{ review.title }}</h3> | |
| 229 | 223 | <p class="mt-1 text-gray-600 text-sm"> |
| 230 | <span :text="review.judge.name"></span> | |
| 224 | <span>{{ review.judge.name }}</span> | |
| 231 | 225 | <span class="text-gray-400"> · </span> |
| 232 | <span :if="review.user" :text="review.user ? review.user.email : ''"></span> | |
| 226 | <span :if="review.user">{{ review.user ? review.user.email : '' }}</span> | |
| 233 | 227 | <span :if="!review.user" class="text-gray-400">(anonymous)</span> |
| 234 | 228 | <span class="text-gray-400"> · </span> |
| 235 | <span :text="'★ ' + review.rating"></span> | |
| 229 | <span>{{ '★ ' + review.rating }}</span> | |
| 236 | 230 | </p> |
| 237 | 231 | </div> |
| 238 | 232 | <div class="flex flex-shrink-0 flex-col gap-y-1 items-end"> |
| @@ -258,9 +252,7 @@ | ||
| 258 | 252 | <button |
| 259 | 253 | type="button" |
| 260 | 254 | @click="toggleExpand(review)" |
| 261 | class="text-gray-500 text-xs hover:text-gray-900" | |
| 262 | :text="expandedId() === review.id ? 'Hide content' : 'Show content'" | |
| 263 | ></button> | |
| 255 | class="text-gray-500 text-xs hover:text-gray-900">{{ expandedId() === review.id ? 'Hide content' : 'Show content' }}</button> | |
| 264 | 256 | </div> |
| 265 | 257 | </div> |
| 266 | 258 | <div class="mt-3 space-y-3" x-class="expandedId() === review.id ? '' : 'hidden'"> |
| @@ -285,7 +277,7 @@ | ||
| 285 | 277 | class="inline-flex items-center justify-center px-3 py-1.5 font-semibold text-white text-xs bg-gray-900 hover:bg-gray-800 rounded disabled:opacity-50" |
| 286 | 278 | > |
| 287 | 279 | <svg :if="savingResponse()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 288 | <span :text="savingResponse() ? 'Saving…' : 'Save response'"></span> | |
| 280 | <span>{{ savingResponse() ? 'Saving…' : 'Save response' }}</span> | |
| 289 | 281 | </button> |
| 290 | 282 | <button |
| 291 | 283 | type="button" |
| @@ -302,7 +294,7 @@ | ||
| 302 | 294 | |
| 303 | 295 | <div class="flex justify-between items-center"> |
| 304 | 296 | <p class="text-gray-500 text-sm"> |
| 305 | Page <span :text="store.page()"></span> of <span :text="store.totalPages()"></span> | |
| 297 | Page <span>{{ store.page() }}</span> of <span>{{ store.totalPages() }}</span> | |
| 306 | 298 | </p> |
| 307 | 299 | <div class="flex gap-x-2"> |
| 308 | 300 | <button |
| @@ -127,7 +127,7 @@ | ||
| 127 | 127 | <div> |
| 128 | 128 | <h1 class="font-bold text-2xl text-gray-900">Users</h1> |
| 129 | 129 | <p class="mt-1 text-gray-600 text-sm"> |
| 130 | <span :text="store.total()"></span> total | |
| 130 | <span>{{ store.total() }}</span> total | |
| 131 | 131 | </p> |
| 132 | 132 | </div> |
| 133 | 133 | <div> |
| @@ -145,9 +145,7 @@ | ||
| 145 | 145 | <div |
| 146 | 146 | role="alert" |
| 147 | 147 | class="px-4 py-3 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 148 | x-class="store.actionError() ? '' : 'hidden'" | |
| 149 | :text="store.actionError()" | |
| 150 | ></div> | |
| 148 | x-class="store.actionError() ? '' : 'hidden'">{{ store.actionError() }}</div> | |
| 151 | 149 | |
| 152 | 150 | <div class="bg-white border border-gray-200 rounded-lg overflow-hidden"> |
| 153 | 151 | <table class="w-full divide-gray-200 divide-y"> |
| @@ -175,20 +173,20 @@ | ||
| 175 | 173 | <path d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m6 5.87a4 4 0 100-8 4 4 0 000 8zm0 0a4 4 0 014 4M5 8a3 3 0 116 0 3 3 0 01-6 0zm10 0a3 3 0 116 0 3 3 0 01-6 0z" /> |
| 176 | 174 | </svg> |
| 177 | 175 | </div> |
| 178 | <p class="font-semibold text-gray-900 text-sm" :text="emptyTitle()"></p> | |
| 179 | <p class="mt-1 max-w-md mx-auto text-gray-500 text-xs leading-relaxed" :text="emptyHint()"></p> | |
| 176 | <p class="font-semibold text-gray-900 text-sm">{{ emptyTitle() }}</p> | |
| 177 | <p class="mt-1 max-w-md mx-auto text-gray-500 text-xs leading-relaxed">{{ emptyHint() }}</p> | |
| 180 | 178 | </td> |
| 181 | 179 | </tr> |
| 182 | 180 | <tr :for="user in store.users" class="hover:bg-gray-50"> |
| 183 | 181 | <td class="px-4 py-3 text-gray-900 text-sm"> |
| 184 | 182 | <div class="flex flex-col"> |
| 185 | <span class="font-medium" :text="user.name || '(no name)'"></span> | |
| 186 | <span class="text-gray-500 text-xs">id <span :text="user.id"></span></span> | |
| 183 | <span class="font-medium">{{ user.name || '(no name)' }}</span> | |
| 184 | <span class="text-gray-500 text-xs">id <span>{{ user.id }}</span></span> | |
| 187 | 185 | </div> |
| 188 | 186 | </td> |
| 189 | <td class="px-4 py-3 text-gray-700 text-sm" :text="user.email"></td> | |
| 190 | <td class="px-4 py-3 text-gray-700 text-sm" :text="rolesText(user)"></td> | |
| 191 | <td class="px-4 py-3 text-gray-500 text-sm" :text="formatDate(user.created_at)"></td> | |
| 187 | <td class="px-4 py-3 text-gray-700 text-sm">{{ user.email }}</td> | |
| 188 | <td class="px-4 py-3 text-gray-700 text-sm">{{ rolesText(user) }}</td> | |
| 189 | <td class="px-4 py-3 text-gray-500 text-sm">{{ formatDate(user.created_at) }}</td> | |
| 192 | 190 | <td class="px-4 py-3 text-right text-sm"> |
| 193 | 191 | <div class="flex justify-end gap-x-2"> |
| 194 | 192 | <button |
| @@ -201,9 +199,7 @@ | ||
| 201 | 199 | @click="togglePromote(user)" |
| 202 | 200 | x-disabled="isSelf(user) && userHasAdmin(user)" |
| 203 | 201 | class="font-medium text-sm hover:underline disabled:cursor-not-allowed disabled:opacity-40" |
| 204 | x-class="userHasAdmin(user) ? 'text-amber-700' : 'text-emerald-700'" | |
| 205 | :text="userHasAdmin(user) ? 'Demote' : 'Promote'" | |
| 206 | ></button> | |
| 202 | x-class="userHasAdmin(user) ? 'text-amber-700' : 'text-emerald-700'">{{ userHasAdmin(user) ? 'Demote' : 'Promote' }}</button> | |
| 207 | 203 | <button |
| 208 | 204 | type="button" |
| 209 | 205 | @click="confirmDelete(user)" |
| @@ -219,7 +215,7 @@ | ||
| 219 | 215 | |
| 220 | 216 | <div class="flex justify-between items-center"> |
| 221 | 217 | <p class="text-gray-500 text-sm"> |
| 222 | Page <span :text="store.page()"></span> of <span :text="store.totalPages()"></span> | |
| 218 | Page <span>{{ store.page() }}</span> of <span>{{ store.totalPages() }}</span> | |
| 223 | 219 | </p> |
| 224 | 220 | <div class="flex gap-x-2"> |
| 225 | 221 | <button |
| @@ -258,12 +254,10 @@ | ||
| 258 | 254 | <div |
| 259 | 255 | role="alert" |
| 260 | 256 | class="px-3 py-2 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 261 | x-class="editError() ? '' : 'hidden'" | |
| 262 | :text="editError()" | |
| 263 | ></div> | |
| 257 | x-class="editError() ? '' : 'hidden'">{{ editError() }}</div> | |
| 264 | 258 | <div class="flex justify-end gap-x-2"> |
| 265 | 259 | <button type="button" @click="cancelEdit()" x-disabled="saving" class="px-3 py-1.5 font-medium text-gray-700 text-sm bg-white border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">Cancel</button> |
| 266 | <button type="button" @click="saveEdit()" x-disabled="saving" class="inline-flex items-center justify-center px-3 py-1.5 font-medium text-white text-sm bg-gray-900 hover:bg-gray-800 rounded disabled:opacity-50 disabled:cursor-not-allowed"><svg :if="saving()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg><span :text="saving() ? 'Saving…' : 'Save'"></span></button> | |
| 260 | <button type="button" @click="saveEdit()" x-disabled="saving" class="inline-flex items-center justify-center px-3 py-1.5 font-medium text-white text-sm bg-gray-900 hover:bg-gray-800 rounded disabled:opacity-50 disabled:cursor-not-allowed"><svg :if="saving()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg><span>{{ saving() ? 'Saving…' : 'Save' }}</span></button> | |
| 267 | 261 | </div> |
| 268 | 262 | </div> |
| 269 | 263 | </div> |
| @@ -7,7 +7,7 @@ | ||
| 7 | 7 | <div class="flex items-center"> |
| 8 | 8 | <Icon :for="star in reviewStars" name="star" size="20" x-class="star <= review.rating ? 'text-yellow-400' : 'text-gray-300'" /> |
| 9 | 9 | </div> |
| 10 | <span class="ml-2 text-gray-500 text-sm" :text="`(${review.rating})`"></span> | |
| 10 | <span class="ml-2 text-gray-500 text-sm">{{ `(${review.rating})` }}</span> | |
| 11 | 11 | </div> |
| 12 | 12 | |
| 13 | 13 | <div class="relative review-content"> |
| @@ -23,11 +23,11 @@ | ||
| 23 | 23 | <div class="flex items-center space-x-4"> |
| 24 | 24 | <button class="inline-flex items-center space-x-2 text-gray-600 hover:text-gray-900"> |
| 25 | 25 | <Icon name="heart" size="20" /> |
| 26 | <span :text="review.likes"></span> | |
| 26 | <span>{{ review.likes }}</span> | |
| 27 | 27 | </button> |
| 28 | 28 | <button class="inline-flex items-center space-x-2 text-gray-600 hover:text-gray-900"> |
| 29 | 29 | <Icon name="message-square" size="20" /> |
| 30 | <span :text="review.comments"></span> | |
| 30 | <span>{{ review.comments }}</span> | |
| 31 | 31 | </button> |
| 32 | 32 | </div> |
| 33 | 33 | </div> |
| @@ -56,7 +56,7 @@ | ||
| 56 | 56 | <div class="mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl"> |
| 57 | 57 | <div class="pt-8 border-gray-200 border-t"> |
| 58 | 58 | <h2 class="font-semibold text-gray-900 text-xl"> |
| 59 | Comments <span class="ml-2 text-base text-gray-500" x-text="`(${comments().length})`"></span> | |
| 59 | Comments <span class="ml-2 text-base text-gray-500">{{ `(${comments().length})` }}</span> | |
| 60 | 60 | </h2> |
| 61 | 61 | |
| 62 | 62 | <!-- Comment form --> |
| @@ -92,10 +92,10 @@ | ||
| 92 | 92 | <img class="flex-none h-10 w-10 rounded-full" x-src="comment.author.imageUrl" x-alt="comment.author.name"> |
| 93 | 93 | <div class="flex-auto"> |
| 94 | 94 | <div class="flex items-center justify-between"> |
| 95 | <p class="font-semibold text-gray-900 text-sm" x-text="comment.author.name"></p> | |
| 96 | <p class="text-gray-500 text-xs" x-text="comment.time"></p> | |
| 95 | <p class="font-semibold text-gray-900 text-sm">{{ comment.author.name }}</p> | |
| 96 | <p class="text-gray-500 text-xs">{{ comment.time }}</p> | |
| 97 | 97 | </div> |
| 98 | <p class="mt-1 leading-6 text-gray-700 text-sm" x-text="comment.content"></p> | |
| 98 | <p class="mt-1 leading-6 text-gray-700 text-sm">{{ comment.content }}</p> | |
| 99 | 99 | <div class="flex gap-x-4 items-center mt-3 text-gray-500 text-xs"> |
| 100 | 100 | <button |
| 101 | 101 | type="button" |
| @@ -103,7 +103,7 @@ | ||
| 103 | 103 | @click="likeComment(comment.id)" |
| 104 | 104 | > |
| 105 | 105 | <Icon name="bookmark" size="16" class="mr-1" /> |
| 106 | <span x-text="comment.likes"></span> | |
| 106 | <span>{{ comment.likes }}</span> | |
| 107 | 107 | </button> |
| 108 | 108 | <button type="button" class="hover:text-gray-700">Reply</button> |
| 109 | 109 | <button type="button" class="hover:text-gray-700">Report</button> |
| @@ -258,9 +258,7 @@ | ||
| 258 | 258 | <div |
| 259 | 259 | role="alert" |
| 260 | 260 | class="px-3 py-2 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 261 | x-class="editError() ? '' : 'hidden'" | |
| 262 | :text="editError()" | |
| 263 | ></div> | |
| 261 | x-class="editError() ? '' : 'hidden'">{{ editError() }}</div> | |
| 264 | 262 | |
| 265 | 263 | <div class="flex justify-end gap-x-2 pt-2"> |
| 266 | 264 | <button |
| @@ -276,7 +274,7 @@ | ||
| 276 | 274 | class="inline-flex items-center justify-center px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50" |
| 277 | 275 | > |
| 278 | 276 | <svg :if="editSaving()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 279 | <span :text="editSaving() ? 'Saving…' : 'Save & resubmit'"></span> | |
| 277 | <span>{{ editSaving() ? 'Saving…' : 'Save & resubmit' }}</span> | |
| 280 | 278 | </button> |
| 281 | 279 | </div> |
| 282 | 280 | </div> |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | <header class="relative"> |
| 2 | 2 | <div class="mx-auto pb-8 pt-16 px-4 sm:px-6 lg:px-8 max-w-5xl"> |
| 3 | 3 | <div class="text-center"> |
| 4 | <h1 class="font-bold text-4xl text-gray-900 tracking-tight sm:text-5xl" :text="review.title"></h1> | |
| 4 | <h1 class="font-bold text-4xl text-gray-900 tracking-tight sm:text-5xl">{{ review.title }}</h1> | |
| 5 | 5 | </div> |
| 6 | 6 | |
| 7 | 7 | <!-- Author and Judge info --> |
| @@ -10,23 +10,23 @@ | ||
| 10 | 10 | <img class="h-12 w-12 rounded-full" x-src="review.author.imageUrl" x-alt="review.author.name"> |
| 11 | 11 | <div class="ml-3"> |
| 12 | 12 | <p class="font-medium text-gray-900 text-sm"> |
| 13 | <a href="#" class="hover:underline" :text="review.author.name"></a> | |
| 13 | <a href="#" class="hover:underline">{{ review.author.name }}</a> | |
| 14 | 14 | </p> |
| 15 | 15 | <div class="flex space-x-1 text-gray-500 text-sm"> |
| 16 | <time :datetime="review.dateTime" :text="review.date"></time> | |
| 16 | <time :datetime="review.dateTime">{{ review.date }}</time> | |
| 17 | 17 | <span aria-hidden="true">·</span> |
| 18 | 18 | </div> |
| 19 | 19 | </div> |
| 20 | 20 | </div> |
| 21 | 21 | <div class="text-right"> |
| 22 | 22 | <div class="flex flex-col items-end"> |
| 23 | <span class="inline-flex items-center mb-2 px-2 py-1 font-medium text-green-700 text-xs bg-green-50 ring-1 ring-green-600/20 ring-inset rounded-full" :text="review.type"></span> | |
| 23 | <span class="inline-flex items-center mb-2 px-2 py-1 font-medium text-green-700 text-xs bg-green-50 ring-1 ring-green-600/20 ring-inset rounded-full">{{ review.type }}</span> | |
| 24 | 24 | <h2 class="font-semibold text-gray-900 text-lg">Reviewing</h2> |
| 25 | 25 | <div class="flex items-center justify-end mt-1"> |
| 26 | 26 | <img class="h-10 w-10 rounded-full" x-src="review.judge.photo" x-alt="review.judge.name"> |
| 27 | 27 | <div class="ml-3"> |
| 28 | <p class="font-medium text-gray-900 text-sm" :text="review.judge.name"></p> | |
| 29 | <p class="text-gray-500 text-sm" :text="review.judge.court"></p> | |
| 28 | <p class="font-medium text-gray-900 text-sm">{{ review.judge.name }}</p> | |
| 29 | <p class="text-gray-500 text-sm">{{ review.judge.court }}</p> | |
| 30 | 30 | </div> |
| 31 | 31 | </div> |
| 32 | 32 | </div> |
| @@ -479,8 +479,8 @@ | ||
| 479 | 479 | > |
| 480 | 480 | <div class="flex flex-wrap items-start justify-between gap-3"> |
| 481 | 481 | <div class="flex-1 min-w-0"> |
| 482 | <p class="font-semibold text-sm" :text="statusLabel"></p> | |
| 483 | <p class="mt-1 text-xs leading-relaxed" :text="statusBlurb"></p> | |
| 482 | <p class="font-semibold text-sm">{{ statusLabel }}</p> | |
| 483 | <p class="mt-1 text-xs leading-relaxed">{{ statusBlurb }}</p> | |
| 484 | 484 | </div> |
| 485 | 485 | <div class="flex flex-shrink-0 gap-x-2"> |
| 486 | 486 | <button |
| @@ -495,7 +495,7 @@ | ||
| 495 | 495 | class="inline-flex items-center justify-center px-3 py-1.5 font-semibold text-red-700 text-xs bg-white border border-red-200 rounded-md hover:bg-red-50 disabled:opacity-50" |
| 496 | 496 | > |
| 497 | 497 | <svg :if="deleting()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 498 | <span :text="deleting() ? 'Deleting…' : 'Delete'"></span> | |
| 498 | <span>{{ deleting() ? 'Deleting…' : 'Delete' }}</span> | |
| 499 | 499 | </button> |
| 500 | 500 | </div> |
| 501 | 501 | </div> |
| @@ -507,15 +507,13 @@ | ||
| 507 | 507 | <div |
| 508 | 508 | role="alert" |
| 509 | 509 | class="mb-6 px-3 py-2 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 510 | x-class="editError() ? '' : 'hidden'" | |
| 511 | :text="editError()" | |
| 512 | ></div> | |
| 510 | x-class="editError() ? '' : 'hidden'">{{ editError() }}</div> | |
| 513 | 511 | |
| 514 | 512 | <!-- Topic chip --> |
| 515 | <p class="font-medium text-amber-600 text-center text-xs tracking-widest uppercase" :text="reviewTypeLabel"></p> | |
| 513 | <p class="font-medium text-amber-600 text-center text-xs tracking-widest uppercase">{{ reviewTypeLabel }}</p> | |
| 516 | 514 | |
| 517 | 515 | <!-- Hero title — large serif, tight tracking, generous bottom margin --> |
| 518 | <h1 class="mt-4 font-serif font-bold leading-[1.1] text-4xl sm:text-5xl lg:text-6xl text-center text-gray-900 tracking-tight" :text="title"></h1> | |
| 516 | <h1 class="mt-4 font-serif font-bold leading-[1.1] text-4xl sm:text-5xl lg:text-6xl text-center text-gray-900 tracking-tight">{{ title }}</h1> | |
| 519 | 517 | |
| 520 | 518 | <!-- Star rating + date + read time row. Centered, subtle. --> |
| 521 | 519 | <div class="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 mt-8 text-gray-500 text-sm"> |
| @@ -529,12 +527,12 @@ | ||
| 529 | 527 | <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.12 2.12 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16z" /> |
| 530 | 528 | </svg> |
| 531 | 529 | </span> |
| 532 | <span class="ml-2 font-medium text-gray-700 tabular-nums" :text="`${rating}/5`"></span> | |
| 530 | <span class="ml-2 font-medium text-gray-700 tabular-nums">{{ `${rating}/5` }}</span> | |
| 533 | 531 | </div> |
| 534 | 532 | <span class="hidden sm:inline text-gray-300">·</span> |
| 535 | <span :text="date"></span> | |
| 533 | <span>{{ date }}</span> | |
| 536 | 534 | <span class="hidden sm:inline text-gray-300">·</span> |
| 537 | <span :text="`${readMinutes} min read`"></span> | |
| 535 | <span>{{ `${readMinutes} min read` }}</span> | |
| 538 | 536 | <!-- Reviewer line. Three rendering modes (bench-review#29, #36): |
| 539 | 537 | - Linked: real user → "by <Name>" with click-through to /user/:id |
| 540 | 538 | - Plain: anonymized → "by Anonymous attorney" with no link |
| @@ -542,9 +540,9 @@ | ||
| 542 | 540 | <span :if="hasReviewerName" class="hidden sm:inline text-gray-300">·</span> |
| 543 | 541 | <span :if="hasReviewerLink" class="text-gray-500"> |
| 544 | 542 | by |
| 545 | <StxLink x-to="`/user/${reviewerId}`" class="ml-1 text-gray-700 hover:underline underline-offset-2" :text="reviewerName"></StxLink> | |
| 543 | <StxLink x-to="`/user/${reviewerId}`" class="ml-1 text-gray-700 hover:underline underline-offset-2">{{ reviewerName }}</StxLink> | |
| 546 | 544 | </span> |
| 547 | <span :if="hasReviewerName && !hasReviewerLink()" class="text-gray-500" :text="`by ${reviewerName()}`"></span> | |
| 545 | <span :if="hasReviewerName && !hasReviewerLink()" class="text-gray-500">{{ `by ${reviewerName()}` }}</span> | |
| 548 | 546 | </div> |
| 549 | 547 | |
| 550 | 548 | <!-- Judge byline. Medium-style author row: round avatar + name + court. |
| @@ -560,16 +558,14 @@ | ||
| 560 | 558 | /> |
| 561 | 559 | <div |
| 562 | 560 | :if="!judgeImage" |
| 563 | class="flex items-center justify-center h-12 w-12 font-semibold text-amber-700 text-sm bg-amber-50 ring-1 ring-amber-200/60 rounded-full" | |
| 564 | :text="judgeInitials" | |
| 565 | ></div> | |
| 561 | class="flex items-center justify-center h-12 w-12 font-semibold text-amber-700 text-sm bg-amber-50 ring-1 ring-amber-200/60 rounded-full">{{ judgeInitials }}</div> | |
| 566 | 562 | <div class="min-w-0 flex-1"> |
| 567 | 563 | <p class="font-semibold text-gray-900 text-sm"> |
| 568 | 564 | <span class="text-gray-400 font-normal">Review of</span> |
| 569 | <StxLink :if="judgeId" x-to="`/judges/${judgeId}/profile`" class="ml-1 text-gray-900 hover:underline underline-offset-2" :text="judgeName"></StxLink> | |
| 570 | <span :if="!judgeId" :text="judgeName"></span> | |
| 565 | <StxLink :if="judgeId" x-to="`/judges/${judgeId}/profile`" class="ml-1 text-gray-900 hover:underline underline-offset-2">{{ judgeName }}</StxLink> | |
| 566 | <span :if="!judgeId">{{ judgeName }}</span> | |
| 571 | 567 | </p> |
| 572 | <p :if="judgeCourt" class="text-gray-500 text-xs mt-0.5" :text="judgeCourt"></p> | |
| 568 | <p :if="judgeCourt" class="text-gray-500 text-xs mt-0.5">{{ judgeCourt }}</p> | |
| 573 | 569 | </div> |
| 574 | 570 | <StxLink |
| 575 | 571 | :if="judgeId" |
| @@ -619,7 +615,7 @@ | ||
| 619 | 615 | x-class="hasJudgeResponse() ? '' : 'hidden'" |
| 620 | 616 | > |
| 621 | 617 | <p class="font-semibold text-amber-800 text-xs tracking-widest uppercase">Official response</p> |
| 622 | <p class="mt-1 font-semibold text-gray-900 text-sm" :text="`From ${judgeResponseName()}`"></p> | |
| 618 | <p class="mt-1 font-semibold text-gray-900 text-sm">{{ `From ${judgeResponseName()}` }}</p> | |
| 623 | 619 | <div class="article-body mt-3 text-base text-gray-800" x-html="judgeResponseBody"></div> |
| 624 | 620 | </div> |
| 625 | 621 | |
| @@ -643,10 +639,8 @@ | ||
| 643 | 639 | type="button" |
| 644 | 640 | @click="postJudgeResponse()" |
| 645 | 641 | x-disabled="judgeResponding" |
| 646 | class="px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50" | |
| 647 | :text="judgeResponding() ? 'Posting…' : 'Post response'" | |
| 648 | ></button> | |
| 649 | <span class="text-red-700 text-xs" x-class="judgeRespondError() ? '' : 'hidden'" :text="judgeRespondError()"></span> | |
| 642 | class="px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50">{{ judgeResponding() ? 'Posting…' : 'Post response' }}</button> | |
| 643 | <span class="text-red-700 text-xs" x-class="judgeRespondError() ? '' : 'hidden'">{{ judgeRespondError() }}</span> | |
| 650 | 644 | </div> |
| 651 | 645 | </div> |
| 652 | 646 | |
| @@ -679,7 +673,7 @@ | ||
| 679 | 673 | <!-- Caption shifts based on count + own-like state so the |
| 680 | 674 | button reads naturally at 0, 1, and N — and credits the |
| 681 | 675 | reader directly when they're the one who reacted. --> |
| 682 | <span class="text-xs text-current/80" :text="liked ? (likes === 1 ? 'You find this helpful' : `You and ${likes - 1} ${likes === 2 ? 'other' : 'others'} find this helpful`) : (likes === 0 ? 'Be the first to find this helpful' : likes === 1 ? 'One person finds this helpful' : `${likes} people find this helpful`)"></span> | |
| 676 | <span class="text-xs text-current/80">{{ liked ? (likes === 1 ? 'You find this helpful' : `You and ${likes - 1} ${likes === 2 ? 'other' : 'others'} find this helpful`) : (likes === 0 ? 'Be the first to find this helpful' : likes === 1 ? 'One person finds this helpful' : `${likes} people find this helpful`) }}</span> | |
| 683 | 677 | </button> |
| 684 | 678 | <!-- Author view: read-only badge showing how readers reacted. |
| 685 | 679 | Same icon + count + caption shape, no click handler, no |
| @@ -694,8 +688,8 @@ | ||
| 694 | 688 | <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"> |
| 695 | 689 | <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> |
| 696 | 690 | </svg> |
| 697 | <span class="tabular-nums" :text="likes"></span> | |
| 698 | <span class="text-xs text-gray-500" :text="likes === 0 ? 'No reactions yet' : likes === 1 ? '1 reader found this helpful' : `${likes} readers found this helpful`"></span> | |
| 691 | <span class="tabular-nums">{{ likes }}</span> | |
| 692 | <span class="text-xs text-gray-500">{{ likes === 0 ? 'No reactions yet' : likes === 1 ? '1 reader found this helpful' : `${likes} readers found this helpful` }}</span> | |
| 699 | 693 | </div> |
| 700 | 694 | </div> |
| 701 | 695 | <!-- Inline sign-in nudge (anonymous click). Soft prompt, no |
| @@ -714,9 +708,7 @@ | ||
| 714 | 708 | <div |
| 715 | 709 | :if="likeError" |
| 716 | 710 | role="alert" |
| 717 | class="px-3 py-1.5 text-red-800 text-xs bg-red-50 border border-red-200 rounded-md" | |
| 718 | :text="likeError" | |
| 719 | ></div> | |
| 711 | class="px-3 py-1.5 text-red-800 text-xs bg-red-50 border border-red-200 rounded-md">{{ likeError }}</div> | |
| 720 | 712 | </div> |
| 721 | 713 | |
| 722 | 714 | <div class="flex items-center gap-4 text-gray-500 text-sm"> |
| @@ -726,9 +718,7 @@ | ||
| 726 | 718 | the author check. --> |
| 727 | 719 | <span |
| 728 | 720 | class="hidden sm:inline-flex items-center px-2.5 py-1 font-medium text-xs ring-1 rounded-full capitalize" |
| 729 | x-class="isMine() ? statusBadgeClasses() : 'hidden'" | |
| 730 | :text="statusLabel" | |
| 731 | ></span> | |
| 721 | x-class="isMine() ? statusBadgeClasses() : 'hidden'">{{ statusLabel }}</span> | |
| 732 | 722 | <StxLink x-to="`/reviews`" class="text-gray-600 hover:text-gray-900 hover:underline underline-offset-2"> |
| 733 | 723 | ← All reviews |
| 734 | 724 | </StxLink> |
| @@ -762,7 +752,7 @@ | ||
| 762 | 752 | <div class="mt-2 space-y-1.5"> |
| 763 | 753 | <label :for="opt in flagReasonOptions" class="flex items-center gap-2 px-3 py-2 text-gray-700 text-sm bg-white border border-gray-200 hover:bg-gray-50 rounded-md cursor-pointer transition-colors"> |
| 764 | 754 | <input type="radio" name="flag_reason" x-model="flagReason" :value="opt.value"> |
| 765 | <span :text="opt.label"></span> | |
| 755 | <span>{{ opt.label }}</span> | |
| 766 | 756 | </label> |
| 767 | 757 | </div> |
| 768 | 758 | </div> |
| @@ -781,9 +771,7 @@ | ||
| 781 | 771 | <div |
| 782 | 772 | role="alert" |
| 783 | 773 | class="mt-3 px-3 py-2 text-red-800 text-xs bg-red-50 border border-red-200 rounded-md" |
| 784 | x-class="flagError() ? '' : 'hidden'" | |
| 785 | :text="flagError()" | |
| 786 | ></div> | |
| 774 | x-class="flagError() ? '' : 'hidden'">{{ flagError() }}</div> | |
| 787 | 775 | |
| 788 | 776 | <div |
| 789 | 777 | role="status" |
| @@ -801,9 +789,7 @@ | ||
| 801 | 789 | type="button" |
| 802 | 790 | @click="submitFlag()" |
| 803 | 791 | x-disabled="flagSubmitting" |
| 804 | class="px-3 py-1.5 font-medium text-white text-sm bg-red-600 hover:bg-red-700 rounded-md disabled:opacity-50" | |
| 805 | :text="flagSubmitting() ? 'Sending…' : 'Send report'" | |
| 806 | ></button> | |
| 792 | class="px-3 py-1.5 font-medium text-white text-sm bg-red-600 hover:bg-red-700 rounded-md disabled:opacity-50">{{ flagSubmitting() ? 'Sending…' : 'Send report' }}</button> | |
| 807 | 793 | </div> |
| 808 | 794 | </div> |
| 809 | 795 | </div> |
| @@ -109,21 +109,19 @@ | ||
| 109 | 109 | </div> |
| 110 | 110 | <button type="submit" x-disabled="loading" |
| 111 | 111 | class="flex-none px-3.5 py-2.5 font-semibold text-sm text-white bg-gray-600 hover:bg-gray-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-gray-600 focus-visible:outline-offset-2 shadow-sm" |
| 112 | :text="loading() ? 'Subscribing...' : 'Notify me'"> | |
| 113 | Notify me | |
| 114 | </button> | |
| 112 | >{{ loading() ? 'Subscribing...' : 'Notify me' }}</button> | |
| 115 | 113 | </form> |
| 116 | 114 | </div> |
| 117 | 115 | <div :if="successVisible()" @close="successVisible.set(false)"> |
| 118 | 116 | <Notification type="success" position="top-right" :show="true" :duration="0"> |
| 119 | <p class="text-sm font-medium" :text="toastTitle()"></p> | |
| 120 | <p class="mt-1 text-sm opacity-90" :text="toastMessage()"></p> | |
| 117 | <p class="text-sm font-medium">{{ toastTitle() }}</p> | |
| 118 | <p class="mt-1 text-sm opacity-90">{{ toastMessage() }}</p> | |
| 121 | 119 | </Notification> |
| 122 | 120 | </div> |
| 123 | 121 | <div :if="errorVisible()" @close="errorVisible.set(false)"> |
| 124 | 122 | <Notification type="error" position="top-right" :show="true" :duration="0"> |
| 125 | <p class="text-sm font-medium" :text="toastTitle()"></p> | |
| 126 | <p class="mt-1 text-sm opacity-90" :text="toastMessage()"></p> | |
| 123 | <p class="text-sm font-medium">{{ toastTitle() }}</p> | |
| 124 | <p class="mt-1 text-sm opacity-90">{{ toastMessage() }}</p> | |
| 127 | 125 | </Notification> |
| 128 | 126 | </div> |
| 129 | 127 | </div> |
| @@ -335,8 +335,8 @@ | ||
| 335 | 335 | > |
| 336 | 336 | <img class="h-8 w-8 rounded-full object-cover" x-src="judge.photo" x-alt="judge.name"> |
| 337 | 337 | <div class="flex-1 min-w-0"> |
| 338 | <p class="font-medium text-gray-900 text-sm truncate" :text="judge.name"></p> | |
| 339 | <p class="text-gray-500 text-xs truncate" :text="(judge.court && judge.court.name) ? judge.court.name : (judge.location || '')"></p> | |
| 338 | <p class="font-medium text-gray-900 text-sm truncate">{{ judge.name }}</p> | |
| 339 | <p class="text-gray-500 text-xs truncate">{{ (judge.court && judge.court.name) ? judge.court.name : (judge.location || '') }}</p> | |
| 340 | 340 | </div> |
| 341 | 341 | </button> |
| 342 | 342 | </div> |
| @@ -352,8 +352,8 @@ | ||
| 352 | 352 | <svg class="h-4 w-4 text-gray-500" viewBox="0 0 20 20" fill="currentColor"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3A1 1 0 003 7v11a1 1 0 001 1h12a1 1 0 001-1V7a1 1 0 00-.606-.92l-7-3zM5 9h2v6H5V9zm6 0h2v6h-2V9z"/></svg> |
| 353 | 353 | </div> |
| 354 | 354 | <div class="flex-1 min-w-0"> |
| 355 | <p class="font-medium text-gray-900 text-sm truncate" :text="court.name"></p> | |
| 356 | <p class="text-gray-500 text-xs truncate" :text="(court.city || '') + (court.state ? ', ' + court.state : '')"></p> | |
| 355 | <p class="font-medium text-gray-900 text-sm truncate">{{ court.name }}</p> | |
| 356 | <p class="text-gray-500 text-xs truncate">{{ (court.city || '') + (court.state ? ', ' + court.state : '') }}</p> | |
| 357 | 357 | </div> |
| 358 | 358 | </button> |
| 359 | 359 | </div> |
| @@ -408,9 +408,7 @@ | ||
| 408 | 408 | would render even with zero unread. --> |
| 409 | 409 | <span |
| 410 | 410 | x-if="notifications.unreadCount()" |
| 411 | class="absolute -top-0.5 -right-0.5 inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 font-semibold text-[10px] text-white bg-red-500 rounded-full ring-2 ring-white" | |
| 412 | :text="notifications.unreadCount() > 99 ? '99+' : notifications.unreadCount()" | |
| 413 | ></span> | |
| 411 | class="absolute -top-0.5 -right-0.5 inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 font-semibold text-[10px] text-white bg-red-500 rounded-full ring-2 ring-white">{{ notifications.unreadCount() > 99 ? '99+' : notifications.unreadCount() }}</span> | |
| 414 | 412 | </button> |
| 415 | 413 | |
| 416 | 414 | <!-- Dropdown panel. Same x-class-hidden treatment as the |
| @@ -449,8 +447,8 @@ | ||
| 449 | 447 | unread; keeps the row scannable. --> |
| 450 | 448 | <span class="mt-1 inline-flex h-2 w-2 flex-shrink-0 rounded-full" x-class="n.unread ? 'bg-blue-500' : 'bg-transparent'"></span> |
| 451 | 449 | <div class="flex-1 min-w-0"> |
| 452 | <p class="text-gray-900 text-sm" :text="notificationLabel(n)"></p> | |
| 453 | <p class="mt-0.5 text-gray-400 text-xs" :text="relativeTime(n.created_at)"></p> | |
| 450 | <p class="text-gray-900 text-sm">{{ notificationLabel(n) }}</p> | |
| 451 | <p class="mt-0.5 text-gray-400 text-xs">{{ relativeTime(n.created_at) }}</p> | |
| 454 | 452 | </div> |
| 455 | 453 | </button> |
| 456 | 454 | </li> |
| @@ -476,10 +474,8 @@ | ||
| 476 | 474 | x-aria-expanded="userMenuOpen() ? 'true' : 'false'" |
| 477 | 475 | > |
| 478 | 476 | <span |
| 479 | class="inline-flex items-center justify-center h-8 w-8 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full" | |
| 480 | :text="userInitials" | |
| 481 | ></span> | |
| 482 | <span class="hidden sm:inline font-semibold text-gray-900 text-sm/6 max-w-[10rem] truncate" :text="userName"></span> | |
| 477 | class="inline-flex items-center justify-center h-8 w-8 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full">{{ userInitials }}</span> | |
| 478 | <span class="hidden sm:inline font-semibold text-gray-900 text-sm/6 max-w-[10rem] truncate">{{ userName }}</span> | |
| 483 | 479 | <svg |
| 484 | 480 | class="hidden sm:block text-gray-400 transition-transform" |
| 485 | 481 | x-class="userMenuOpen() ? 'rotate-180' : ''" |
| @@ -498,10 +494,10 @@ | ||
| 498 | 494 | toggling between accounts don't fire an action against |
| 499 | 495 | the wrong session. --> |
| 500 | 496 | <div class="flex gap-3 items-center px-4 py-3 border-b border-gray-100"> |
| 501 | <span class="inline-flex items-center justify-center h-9 w-9 flex-shrink-0 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full" :text="userInitials"></span> | |
| 497 | <span class="inline-flex items-center justify-center h-9 w-9 flex-shrink-0 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full">{{ userInitials }}</span> | |
| 502 | 498 | <div class="flex-1 min-w-0"> |
| 503 | <p class="font-semibold text-gray-900 text-sm truncate" :text="userName"></p> | |
| 504 | <p class="text-gray-500 text-xs truncate" :text="userEmail"></p> | |
| 499 | <p class="font-semibold text-gray-900 text-sm truncate">{{ userName }}</p> | |
| 500 | <p class="text-gray-500 text-xs truncate">{{ userEmail }}</p> | |
| 505 | 501 | </div> |
| 506 | 502 | </div> |
| 507 | 503 | |
| @@ -9,7 +9,7 @@ | ||
| 9 | 9 | <header class="relative"> |
| 10 | 10 | <div class="mx-auto pb-8 pt-16 px-4 sm:px-6 lg:px-8 max-w-5xl"> |
| 11 | 11 | <div class="text-center"> |
| 12 | <h1 class="font-bold text-4xl text-gray-900 tracking-tight sm:text-5xl" :text="post.title"></h1> | |
| 12 | <h1 class="font-bold text-4xl text-gray-900 tracking-tight sm:text-5xl">{{ post.title }}</h1> | |
| 13 | 13 | </div> |
| 14 | 14 | |
| 15 | 15 | <!-- Author and meta info --> |
| @@ -19,16 +19,16 @@ | ||
| 19 | 19 | <img class="h-12 w-12 rounded-full" x-src="post.author.imageUrl" x-alt="post.author.name"> |
| 20 | 20 | <div class="ml-3"> |
| 21 | 21 | <p class="font-medium text-gray-900 text-sm"> |
| 22 | <a href="#" class="hover:underline" :text="post.author.name"></a> | |
| 22 | <a href="#" class="hover:underline">{{ post.author.name }}</a> | |
| 23 | 23 | </p> |
| 24 | 24 | </div> |
| 25 | 25 | </div> |
| 26 | 26 | |
| 27 | 27 | <!-- Reading time and date --> |
| 28 | 28 | <div class="flex items-center space-x-4 text-gray-500 text-sm"> |
| 29 | <span :text="readingTime"></span> | |
| 29 | <span>{{ readingTime }}</span> | |
| 30 | 30 | <span aria-hidden="true">·</span> |
| 31 | <time :datetime="post.dateTime" :text="post.date"></time> | |
| 31 | <time :datetime="post.dateTime">{{ post.date }}</time> | |
| 32 | 32 | </div> |
| 33 | 33 | </div> |
| 34 | 34 | </div> |
| @@ -68,8 +68,8 @@ | ||
| 68 | 68 | class="flex items-center px-3 py-2 font-medium text-sm rounded-md group" |
| 69 | 69 | x-class="activeCategory === category.key ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'" |
| 70 | 70 | > |
| 71 | <span :text="category.label"></span> | |
| 72 | <span class="inline-block ml-auto px-2 py-0.5 font-medium text-gray-600 text-xs bg-gray-100 rounded-full" :text="category.count"></span> | |
| 71 | <span>{{ category.label }}</span> | |
| 72 | <span class="inline-block ml-auto px-2 py-0.5 font-medium text-gray-600 text-xs bg-gray-100 rounded-full">{{ category.count }}</span> | |
| 73 | 73 | </a> |
| 74 | 74 | </nav> |
| 75 | 75 | </div> |
| @@ -174,7 +174,7 @@ | ||
| 174 | 174 | |
| 175 | 175 | <section class="mt-12 pt-8 border-t border-gray-200"> |
| 176 | 176 | <header class="flex items-end justify-between"> |
| 177 | <h2 class="font-semibold text-gray-900 text-base" :text="`Comments (${comments().length})`"></h2> | |
| 177 | <h2 class="font-semibold text-gray-900 text-base">{{ `Comments (${comments().length})` }}</h2> | |
| 178 | 178 | </header> |
| 179 | 179 | |
| 180 | 180 | <!-- Composer slot — one if/else-if/else chain (mutually exclusive, |
| @@ -199,9 +199,7 @@ | ||
| 199 | 199 | <div |
| 200 | 200 | role="alert" |
| 201 | 201 | class="mt-2 px-3 py-2 text-red-800 text-xs bg-red-50 border border-red-200 rounded-md" |
| 202 | x-class="commentError() ? '' : 'hidden'" | |
| 203 | :text="commentError()" | |
| 204 | ></div> | |
| 202 | x-class="commentError() ? '' : 'hidden'">{{ commentError() }}</div> | |
| 205 | 203 | |
| 206 | 204 | <div class="flex flex-wrap items-center justify-between gap-2 mt-3"> |
| 207 | 205 | <label class="flex gap-2 items-center text-gray-600 text-xs cursor-pointer"> |
| @@ -215,7 +213,7 @@ | ||
| 215 | 213 | class="inline-flex items-center justify-center px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50" |
| 216 | 214 | > |
| 217 | 215 | <svg :if="commentSubmitting()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 218 | <span :text="commentSubmitting() ? 'Posting…' : 'Post comment'"></span> | |
| 216 | <span>{{ commentSubmitting() ? 'Posting…' : 'Post comment' }}</span> | |
| 219 | 217 | </button> |
| 220 | 218 | </div> |
| 221 | 219 | </div> |
| @@ -257,15 +255,15 @@ | ||
| 257 | 255 | <ul :else class="space-y-4 mt-6"> |
| 258 | 256 | <li :for="c in comments" class="p-4 bg-white border border-gray-100 rounded-lg"> |
| 259 | 257 | <div class="flex items-start gap-3"> |
| 260 | <span class="inline-flex flex-none items-center justify-center h-8 w-8 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full" :text="initialFor(c.author && c.author.name)"></span> | |
| 258 | <span class="inline-flex flex-none items-center justify-center h-8 w-8 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full">{{ initialFor(c.author && c.author.name) }}</span> | |
| 261 | 259 | <div class="flex-1 min-w-0"> |
| 262 | 260 | <div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5"> |
| 263 | 261 | <!-- Linked when we have a real reviewer id (non-anonymous); plain text otherwise. --> |
| 264 | <StxLink :if="c.author && c.author.id" x-to="`/user/${c.author.id}`" class="font-medium text-gray-900 text-sm hover:underline underline-offset-2" :text="c.author.name"></StxLink> | |
| 265 | <span :if="!(c.author && c.author.id)" class="font-medium text-gray-700 text-sm" :text="c.author && c.author.name || 'Anonymous reviewer'"></span> | |
| 266 | <span class="text-gray-400 text-xs" :text="formatDate(c.created_at)"></span> | |
| 262 | <StxLink :if="c.author && c.author.id" x-to="`/user/${c.author.id}`" class="font-medium text-gray-900 text-sm hover:underline underline-offset-2">{{ c.author.name }}</StxLink> | |
| 263 | <span :if="!(c.author && c.author.id)" class="font-medium text-gray-700 text-sm">{{ c.author && c.author.name || 'Anonymous reviewer' }}</span> | |
| 264 | <span class="text-gray-400 text-xs">{{ formatDate(c.created_at) }}</span> | |
| 267 | 265 | </div> |
| 268 | <p class="mt-1 leading-relaxed text-gray-700 text-sm" :text="c.body"></p> | |
| 266 | <p class="mt-1 leading-relaxed text-gray-700 text-sm">{{ c.body }}</p> | |
| 269 | 267 | <button |
| 270 | 268 | :if="isMyComment(c)" |
| 271 | 269 | type="button" |
| @@ -80,9 +80,9 @@ | ||
| 80 | 80 | <div class="flex gap-x-6 items-center"> |
| 81 | 81 | <img x-src="courtImage" x-alt="courtName" class="object-cover flex-none h-16 w-16 ring-1 ring-gray-900/10 rounded-lg"> |
| 82 | 82 | <div> |
| 83 | <div class="text-gray-500 text-sm/6">Courthouse <span class="text-gray-700" :text="`#${courtHeaderId}`"></span></div> | |
| 84 | <h1 class="mt-1 font-semibold text-base text-gray-900" :text="courtName"></h1> | |
| 85 | <p class="mt-0.5 text-gray-500 text-sm" :text="`${courtCity}${courtCity && courtState ? ', ' : ''}${courtState}`"></p> | |
| 83 | <div class="text-gray-500 text-sm/6">Courthouse <span class="text-gray-700">{{ `#${courtHeaderId}` }}</span></div> | |
| 84 | <h1 class="mt-1 font-semibold text-base text-gray-900">{{ courtName }}</h1> | |
| 85 | <p class="mt-0.5 text-gray-500 text-sm">{{ `${courtCity}${courtCity && courtState ? ', ' : ''}${courtState}` }}</p> | |
| 86 | 86 | </div> |
| 87 | 87 | </div> |
| 88 | 88 | </div> |
| @@ -136,7 +136,7 @@ | ||
| 136 | 136 | x-class="stateFilter === 'all' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 137 | 137 | > |
| 138 | 138 | All states |
| 139 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="stateFilter === 'all' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'" :text="courtCounts.byState.all"></span> | |
| 139 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="stateFilter === 'all' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'">{{ courtCounts.byState.all }}</span> | |
| 140 | 140 | </button> |
| 141 | 141 | <button :for="s in availableStates" |
| 142 | 142 | type="button" |
| @@ -145,8 +145,8 @@ | ||
| 145 | 145 | class="inline-flex items-center gap-2 px-3 py-1.5 font-medium text-sm rounded-full border transition-colors" |
| 146 | 146 | x-class="stateFilter === s ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 147 | 147 | > |
| 148 | <span :text="s"></span> | |
| 149 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="stateFilter === s ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'" :text="courtCounts.byState[s] || 0"></span> | |
| 148 | <span>{{ s }}</span> | |
| 149 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="stateFilter === s ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'">{{ courtCounts.byState[s] || 0 }}</span> | |
| 150 | 150 | </button> |
| 151 | 151 | </div> |
| 152 | 152 | |
| @@ -168,7 +168,7 @@ | ||
| 168 | 168 | x-class="hasReviewsFilter === 'with' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 169 | 169 | > |
| 170 | 170 | With reviews |
| 171 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="hasReviewsFilter === 'with' ? 'bg-white/15 text-white' : 'bg-emerald-50 text-emerald-700'" :text="courtCounts.withReviews"></span> | |
| 171 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="hasReviewsFilter === 'with' ? 'bg-white/15 text-white' : 'bg-emerald-50 text-emerald-700'">{{ courtCounts.withReviews }}</span> | |
| 172 | 172 | </button> |
| 173 | 173 | <button |
| 174 | 174 | type="button" |
| @@ -178,7 +178,7 @@ | ||
| 178 | 178 | x-class="hasReviewsFilter === 'without' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 179 | 179 | > |
| 180 | 180 | Without reviews |
| 181 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="hasReviewsFilter === 'without' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'" :text="courtCounts.withoutReviews"></span> | |
| 181 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="hasReviewsFilter === 'without' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'">{{ courtCounts.withoutReviews }}</span> | |
| 182 | 182 | </button> |
| 183 | 183 | </div> |
| 184 | 184 | |
| @@ -213,16 +213,16 @@ | ||
| 213 | 213 | > |
| 214 | 214 | <div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div> |
| 215 | 215 | <div class="absolute bottom-4 left-4"> |
| 216 | <span class="inline-flex items-center px-3 py-1 font-medium text-gray-700 text-sm bg-white/90 ring-1 ring-gray-700/10 ring-inset rounded-full" :text="court.state"></span> | |
| 216 | <span class="inline-flex items-center px-3 py-1 font-medium text-gray-700 text-sm bg-white/90 ring-1 ring-gray-700/10 ring-inset rounded-full">{{ court.state }}</span> | |
| 217 | 217 | </div> |
| 218 | 218 | </div> |
| 219 | 219 | |
| 220 | 220 | <!-- Content Container --> |
| 221 | 221 | <div class="p-6"> |
| 222 | 222 | <div class="text-center"> |
| 223 | <h3 class="font-semibold text-gray-900 text-xl group-hover:text-deep-brown duration-200 transition-colors" :text="court.name"></h3> | |
| 224 | <p class="mt-2 text-gray-500 text-sm" :text="`${court.city}, ${court.state}`"></p> | |
| 225 | <p class="mt-1 text-gray-500 text-sm" :text="court.address"></p> | |
| 223 | <h3 class="font-semibold text-gray-900 text-xl group-hover:text-deep-brown duration-200 transition-colors">{{ court.name }}</h3> | |
| 224 | <p class="mt-2 text-gray-500 text-sm">{{ `${court.city}, ${court.state}` }}</p> | |
| 225 | <p class="mt-1 text-gray-500 text-sm">{{ court.address }}</p> | |
| 226 | 226 | </div> |
| 227 | 227 | </div> |
| 228 | 228 | </StxLink> |
| @@ -236,7 +236,7 @@ | ||
| 236 | 236 | <svg class="mx-auto h-12 w-12 text-gray-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
| 237 | 237 | <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21" /> |
| 238 | 238 | </svg> |
| 239 | <p class="mt-4 font-medium text-gray-900" :text="(stateFilter() !== 'all' || hasReviewsFilter() !== 'all' || searchQuery() !== '') ? 'No courthouses match these filters' : 'No courthouses yet'"></p> | |
| 239 | <p class="mt-4 font-medium text-gray-900">{{ (stateFilter() !== 'all' || hasReviewsFilter() !== 'all' || searchQuery() !== '') ? 'No courthouses match these filters' : 'No courthouses yet' }}</p> | |
| 240 | 240 | <p x-class="hasActiveFilters() ? '' : 'hidden'" class="mt-1 text-gray-500 text-sm">Try a different state, or clear the filters to see all courthouses.</p> |
| 241 | 241 | <button |
| 242 | 242 | x-class="hasActiveFilters() ? '' : 'hidden'" |
| @@ -19,7 +19,7 @@ | ||
| 19 | 19 | <div class="flex items-center justify-between"> |
| 20 | 20 | <div> |
| 21 | 21 | <h2 class="font-semibold text-2xl text-gray-900">Judges at this Court</h2> |
| 22 | <p class="mt-1 text-gray-500 text-sm" :text="`Active judges currently sitting at ${court.name}`"></p> | |
| 22 | <p class="mt-1 text-gray-500 text-sm">{{ `Active judges currently sitting at ${court.name}` }}</p> | |
| 23 | 23 | </div> |
| 24 | 24 | <div class="hidden sm:block"> |
| 25 | 25 | <select class="pl-3 pr-10 py-2 text-gray-900 sm:text-sm bg-white ring-1 ring-gray-300 ring-inset rounded-md focus:ring-2 focus:ring-gray-600 focus:ring-inset"> |
| @@ -37,14 +37,14 @@ | ||
| 37 | 37 | <StxLink x-to="`/judges/${judge.id}/profile`" class="flex flex-1 gap-x-4 items-center min-w-0"> |
| 38 | 38 | <img class="object-cover flex-none h-12 w-12 rounded-full grayscale filter" x-src="judge.photo" x-alt="judge.name"> |
| 39 | 39 | <div class="flex-auto min-w-0"> |
| 40 | <p class="font-semibold text-gray-900 text-sm hover:underline" :text="judge.name"></p> | |
| 41 | <p class="mt-0.5 text-gray-500 text-xs truncate" :text="judge.department"></p> | |
| 40 | <p class="font-semibold text-gray-900 text-sm hover:underline">{{ judge.name }}</p> | |
| 41 | <p class="mt-0.5 text-gray-500 text-xs truncate">{{ judge.department }}</p> | |
| 42 | 42 | </div> |
| 43 | 43 | </StxLink> |
| 44 | 44 | <div class="flex gap-x-4 items-center sm:gap-x-6"> |
| 45 | 45 | <div class="hidden sm:flex sm:items-center"> |
| 46 | 46 | <Icon :for="star in stars" name="star" size="16" x-class="star <= judge.rating ? 'text-yellow-400' : 'text-gray-200'" /> |
| 47 | <span class="ml-2 text-gray-500 text-xs" :text="judge.reviewCount"></span> | |
| 47 | <span class="ml-2 text-gray-500 text-xs">{{ judge.reviewCount }}</span> | |
| 48 | 48 | </div> |
| 49 | 49 | <StxLink x-to="`/judges/${judge.id}/profile`" class="font-semibold text-gray-700 text-sm hover:text-gray-900"> |
| 50 | 50 | View<span aria-hidden="true"> →</span> |
| @@ -173,7 +173,7 @@ | ||
| 173 | 173 | |
| 174 | 174 | <div class="lg:col-span-2 lg:row-end-2 lg:row-span-2 -mx-4 px-4 py-8 sm:mx-0 sm:pb-14 sm:px-8 xl:pb-20 xl:pt-16 xl:px-16 bg-white ring-1 ring-gray-900/5 sm:rounded-lg shadow-sm"> |
| 175 | 175 | <h2 class="font-semibold text-base text-gray-800">About this court</h2> |
| 176 | <p class="mt-4 leading-7 text-gray-600" :text="aboutText"></p> | |
| 176 | <p class="mt-4 leading-7 text-gray-600">{{ aboutText }}</p> | |
| 177 | 177 | |
| 178 | 178 | <dl class="grid grid-cols-1 sm:grid-cols-2 mt-8 text-sm/6"> |
| 179 | 179 | <div class="sm:pr-4"> |
| @@ -204,22 +204,22 @@ | ||
| 204 | 204 | <dl class="grid gap-4 grid-cols-1 sm:grid-cols-3 mt-4"> |
| 205 | 205 | <div class="px-4 py-3 bg-off-gray rounded-lg"> |
| 206 | 206 | <dt class="text-gray-500 text-xs">Active Judges</dt> |
| 207 | <dd class="mt-1 font-semibold text-2xl text-gray-900" :text="activeJudges"></dd> | |
| 207 | <dd class="mt-1 font-semibold text-2xl text-gray-900">{{ activeJudges }}</dd> | |
| 208 | 208 | </div> |
| 209 | 209 | <div class="px-4 py-3 bg-off-gray rounded-lg"> |
| 210 | 210 | <dt class="text-gray-500 text-xs">Published Reviews</dt> |
| 211 | <dd class="mt-1 font-semibold text-2xl text-gray-900" :text="totalReviews"></dd> | |
| 211 | <dd class="mt-1 font-semibold text-2xl text-gray-900">{{ totalReviews }}</dd> | |
| 212 | 212 | </div> |
| 213 | 213 | <div class="px-4 py-3 bg-off-gray rounded-lg"> |
| 214 | 214 | <dt class="text-gray-500 text-xs">Avg Rating</dt> |
| 215 | <dd class="mt-1 font-semibold text-2xl text-gray-900" :text="avgRating"></dd> | |
| 215 | <dd class="mt-1 font-semibold text-2xl text-gray-900">{{ avgRating }}</dd> | |
| 216 | 216 | </div> |
| 217 | 217 | </dl> |
| 218 | 218 | </div> |
| 219 | 219 | |
| 220 | 220 | <div class="mt-12"> |
| 221 | 221 | <h3 class="font-semibold text-base text-gray-800">Visit this court</h3> |
| 222 | <p class="mt-1 text-gray-500 text-xs" :text="courtAddress"></p> | |
| 222 | <p class="mt-1 text-gray-500 text-xs">{{ courtAddress }}</p> | |
| 223 | 223 | |
| 224 | 224 | <!-- Map host. Leaflet measures the element at construction time, |
| 225 | 225 | so the explicit h-64 (256px) on the host is required — without |
| @@ -54,20 +54,20 @@ | ||
| 54 | 54 | <div :for="review in courtReviews()" class="pb-8 border-b border-gray-200"> |
| 55 | 55 | <div class="flex items-center justify-between"> |
| 56 | 56 | <div class="flex items-center"> |
| 57 | <div class="flex items-center justify-center h-10 w-10 font-semibold text-gray-500 text-sm bg-gray-100 rounded-full" :text="(review.title || 'A').slice(0, 1).toUpperCase()"></div> | |
| 57 | <div class="flex items-center justify-center h-10 w-10 font-semibold text-gray-500 text-sm bg-gray-100 rounded-full">{{ (review.title || 'A').slice(0, 1).toUpperCase() }}</div> | |
| 58 | 58 | <div class="ml-4"> |
| 59 | <h4 class="font-semibold text-gray-900 text-sm" :text="review.title"></h4> | |
| 59 | <h4 class="font-semibold text-gray-900 text-sm">{{ review.title }}</h4> | |
| 60 | 60 | <div class="flex items-center"> |
| 61 | 61 | <div class="flex items-center"> |
| 62 | 62 | <Icon :for="star in stars" name="star" size="16" x-class="star <= review.rating ? 'text-yellow-400' : 'text-gray-300'" /> |
| 63 | 63 | </div> |
| 64 | <span class="ml-2 text-gray-500 text-sm" :text="formatDate(review.created_at)"></span> | |
| 64 | <span class="ml-2 text-gray-500 text-sm">{{ formatDate(review.created_at) }}</span> | |
| 65 | 65 | </div> |
| 66 | 66 | </div> |
| 67 | 67 | </div> |
| 68 | <span :if="review.type" class="inline-flex items-center px-2 py-1 font-medium text-xs text-gray-700 bg-gray-100 ring-1 ring-gray-200 ring-inset rounded-full" :text="review.type"></span> | |
| 68 | <span :if="review.type" class="inline-flex items-center px-2 py-1 font-medium text-xs text-gray-700 bg-gray-100 ring-1 ring-gray-200 ring-inset rounded-full">{{ review.type }}</span> | |
| 69 | 69 | </div> |
| 70 | <p class="mt-3 leading-relaxed line-clamp-3 text-gray-700 text-sm" :text="review.content"></p> | |
| 70 | <p class="mt-3 leading-relaxed line-clamp-3 text-gray-700 text-sm">{{ review.content }}</p> | |
| 71 | 71 | <div class="flex items-center justify-between mt-4"> |
| 72 | 72 | <StxLink x-to="`/judges/${review.judge_id}/profile`" class="text-gray-600 text-sm hover:underline"> |
| 73 | 73 | View judge → |
| @@ -24,19 +24,19 @@ | ||
| 24 | 24 | </StxLink> |
| 25 | 25 | <div class="ml-2"> |
| 26 | 26 | <p class="font-medium text-gray-900 text-sm"> |
| 27 | <StxLink x-to="`/profile/${review.author.id}`" class="hover:underline" :text="review.author.name"></StxLink> | |
| 27 | <StxLink x-to="`/profile/${review.author.id}`" class="hover:underline">{{ review.author.name }}</StxLink> | |
| 28 | 28 | </p> |
| 29 | 29 | <p class="text-gray-500 text-xs">reviewed</p> |
| 30 | 30 | </div> |
| 31 | 31 | </div> |
| 32 | <span class="text-gray-600 text-sm" :text="review.date"></span> | |
| 32 | <span class="text-gray-600 text-sm">{{ review.date }}</span> | |
| 33 | 33 | </div> |
| 34 | 34 | |
| 35 | 35 | <!-- Review Title and Content --> |
| 36 | 36 | <div class="mt-3"> |
| 37 | <h3 class="mb-2 font-bold text-gray-900 text-xl" :text="review.title"></h3> | |
| 37 | <h3 class="mb-2 font-bold text-gray-900 text-xl">{{ review.title }}</h3> | |
| 38 | 38 | <div class="relative"> |
| 39 | <p class="leading-relaxed line-clamp-3 text-base text-gray-700" :text="review.content"></p> | |
| 39 | <p class="leading-relaxed line-clamp-3 text-base text-gray-700">{{ review.content }}</p> | |
| 40 | 40 | @include('Bench/BlurReview') |
| 41 | 41 | </div> |
| 42 | 42 | </div> |
| @@ -44,7 +44,7 @@ | ||
| 44 | 44 | <!-- Judge Name and Rating --> |
| 45 | 45 | <div class="flex items-center justify-between mt-4"> |
| 46 | 46 | <div class="flex items-center space-x-3"> |
| 47 | <StxLink x-to="`/judges/${review.judge.id}/profile`" class="font-semibold text-base text-gray-900 hover:underline" :text="review.judge.name"></StxLink> | |
| 47 | <StxLink x-to="`/judges/${review.judge.id}/profile`" class="font-semibold text-base text-gray-900 hover:underline">{{ review.judge.name }}</StxLink> | |
| 48 | 48 | <div class="flex items-center"> |
| 49 | 49 | <Icon :for="star in stars" name="star" size="16" x-class="star <= review.rating ? 'text-yellow-400' : 'text-gray-300'" /> |
| 50 | 50 | </div> |
| @@ -57,7 +57,7 @@ | ||
| 57 | 57 | x-class="isLiked() ? 'text-blue-600' : 'text-gray-500 hover:text-gray-600'" |
| 58 | 58 | > |
| 59 | 59 | <Icon name="bookmark" size="20" class="mr-1.5 transition-colors" x-class="isLiked() ? 'text-blue-600' : 'text-gray-500 hover:text-gray-600'" /> |
| 60 | <span x-class="isLiked() ? 'text-blue-600' : 'hover:text-gray-600'" :text="review.likes"></span> | |
| 60 | <span x-class="isLiked() ? 'text-blue-600' : 'hover:text-gray-600'">{{ review.likes }}</span> | |
| 61 | 61 | </button> |
| 62 | 62 | <StxLink |
| 63 | 63 | x-to="`/article/${review.id}`" |
| @@ -70,8 +70,8 @@ | ||
| 70 | 70 | |
| 71 | 71 | <!-- Status Tags --> |
| 72 | 72 | <div class="flex items-center mt-4 space-x-2"> |
| 73 | <span class="inline-flex items-center px-2 py-1 font-medium text-deepBrown text-xs bg-offBrown ring-1 ring-deepBrown/20 ring-inset rounded-full" :text="review.type"></span> | |
| 74 | <span class="inline-flex items-center px-2 py-1 font-medium text-deepBrown text-xs bg-offBrown ring-1 ring-deepBrown/20 ring-inset rounded-full" :text="review.status"></span> | |
| 73 | <span class="inline-flex items-center px-2 py-1 font-medium text-deepBrown text-xs bg-offBrown ring-1 ring-deepBrown/20 ring-inset rounded-full">{{ review.type }}</span> | |
| 74 | <span class="inline-flex items-center px-2 py-1 font-medium text-deepBrown text-xs bg-offBrown ring-1 ring-deepBrown/20 ring-inset rounded-full">{{ review.status }}</span> | |
| 75 | 75 | </div> |
| 76 | 76 | </div> |
| 77 | 77 | |
| @@ -107,21 +107,19 @@ | ||
| 107 | 107 | </thead> |
| 108 | 108 | <tbody class="divide-gray-200 divide-y"> |
| 109 | 109 | <tr :for="case_ in cases"> |
| 110 | <td class="pl-4 pr-3 py-4 sm:pl-0 font-medium text-gray-900 text-sm whitespace-nowrap" :text="case_.number"></td> | |
| 111 | <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap" :text="case_.title"></td> | |
| 110 | <td class="pl-4 pr-3 py-4 sm:pl-0 font-medium text-gray-900 text-sm whitespace-nowrap">{{ case_.number }}</td> | |
| 111 | <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap">{{ case_.title }}</td> | |
| 112 | 112 | <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap"> |
| 113 | <span class="inline-flex items-center px-2 py-1 font-medium text-blue-700 text-xs bg-blue-50 ring-1 ring-blue-700/10 ring-inset rounded-md" :text="case_.type"></span> | |
| 113 | <span class="inline-flex items-center px-2 py-1 font-medium text-blue-700 text-xs bg-blue-50 ring-1 ring-blue-700/10 ring-inset rounded-md">{{ case_.type }}</span> | |
| 114 | 114 | </td> |
| 115 | 115 | <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap"> |
| 116 | 116 | <span |
| 117 | 117 | class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset" |
| 118 | x-class="statusClass(case_.status)" | |
| 119 | :text="case_.status" | |
| 120 | ></span> | |
| 118 | x-class="statusClass(case_.status)">{{ case_.status }}</span> | |
| 121 | 119 | </td> |
| 122 | <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap" :text="case_.filedDate"></td> | |
| 120 | <td class="px-3 py-4 text-gray-500 text-sm whitespace-nowrap">{{ case_.filedDate }}</td> | |
| 123 | 121 | <td class="relative pl-3 pr-4 py-4 sm:pr-0 font-medium text-right text-sm whitespace-nowrap"> |
| 124 | <a href="#" class="text-gray-600 hover:text-gray-900">View<span class="sr-only" :text="`, ${case_.number}`"></span></a> | |
| 122 | <a href="#" class="text-gray-600 hover:text-gray-900">View<span class="sr-only">{{ `, ${case_.number}` }}</span></a> | |
| 125 | 123 | </td> |
| 126 | 124 | </tr> |
| 127 | 125 | </tbody> |
| @@ -180,7 +180,7 @@ | ||
| 180 | 180 | x-class="practiceFilter === 'all' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 181 | 181 | > |
| 182 | 182 | All |
| 183 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="practiceFilter === 'all' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'" :text="practiceCounts.all"></span> | |
| 183 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="practiceFilter === 'all' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'">{{ practiceCounts.all }}</span> | |
| 184 | 184 | </button> |
| 185 | 185 | <button :for="area in PRACTICE_AREAS" |
| 186 | 186 | type="button" |
| @@ -189,8 +189,8 @@ | ||
| 189 | 189 | class="inline-flex items-center gap-2 px-3 py-1.5 font-medium text-sm rounded-full border transition-colors capitalize" |
| 190 | 190 | x-class="practiceFilter === area ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 191 | 191 | > |
| 192 | <span :text="area"></span> | |
| 193 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="practiceFilter === area ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'" :text="practiceCounts[area]"></span> | |
| 192 | <span>{{ area }}</span> | |
| 193 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="practiceFilter === area ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'">{{ practiceCounts[area] }}</span> | |
| 194 | 194 | </button> |
| 195 | 195 | </div> |
| 196 | 196 | |
| @@ -230,9 +230,7 @@ | ||
| 230 | 230 | role="tab" |
| 231 | 231 | @click="setStateFilter(s)" |
| 232 | 232 | class="inline-flex items-center px-3 py-1.5 font-medium text-sm rounded-full border transition-colors" |
| 233 | x-class="stateFilter === s ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" | |
| 234 | :text="s" | |
| 235 | ></button> | |
| 233 | x-class="stateFilter === s ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'">{{ s }}</button> | |
| 236 | 234 | </div> |
| 237 | 235 | |
| 238 | 236 | <!-- Rating-threshold + activity filter rows. URL-driven via |
| @@ -325,15 +323,11 @@ | ||
| 325 | 323 | </div> |
| 326 | 324 | |
| 327 | 325 | <span |
| 328 | class="inline-flex items-center mt-3 px-3 py-1 font-medium text-gray-700 text-xs bg-gray-50 ring-1 ring-gray-200 ring-inset rounded-full" | |
| 329 | :text="judge.court.name" | |
| 330 | ></span> | |
| 326 | class="inline-flex items-center mt-3 px-3 py-1 font-medium text-gray-700 text-xs bg-gray-50 ring-1 ring-gray-200 ring-inset rounded-full">{{ judge.court.name }}</span> | |
| 331 | 327 | |
| 332 | 328 | <h3 |
| 333 | class="mt-3 font-semibold text-gray-900 text-lg group-hover:text-gray-700 duration-200 transition-colors" | |
| 334 | :text="judge.name" | |
| 335 | ></h3> | |
| 336 | <p class="mt-1 text-gray-500 text-sm" :text="judge.location"></p> | |
| 329 | class="mt-3 font-semibold text-gray-900 text-lg group-hover:text-gray-700 duration-200 transition-colors">{{ judge.name }}</h3> | |
| 330 | <p class="mt-1 text-gray-500 text-sm">{{ judge.location }}</p> | |
| 337 | 331 | |
| 338 | 332 | <!-- Review aggregates are lazy-loaded by /api/judges/:id/reviews |
| 339 | 333 | when a profile opens — they're NOT included on the |
| @@ -353,7 +347,7 @@ | ||
| 353 | 347 | <svg class="mx-auto h-12 w-12 text-gray-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
| 354 | 348 | <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /> |
| 355 | 349 | </svg> |
| 356 | <p class="mt-4 font-medium text-gray-900" :text="(practiceFilter() !== 'all' || stateFilter() !== 'all' || ratingFilter() !== 'all' || hasReviewsFilter() !== 'all' || searchQuery() !== '') ? 'No judges match these filters' : 'No judges yet'"></p> | |
| 350 | <p class="mt-4 font-medium text-gray-900">{{ (practiceFilter() !== 'all' || stateFilter() !== 'all' || ratingFilter() !== 'all' || hasReviewsFilter() !== 'all' || searchQuery() !== '') ? 'No judges match these filters' : 'No judges yet' }}</p> | |
| 357 | 351 | <p x-class="hasActiveFilters() ? '' : 'hidden'" class="mt-1 text-gray-500 text-sm">Try a different practice area or state, or clear the filters to see all judges.</p> |
| 358 | 352 | <button |
| 359 | 353 | x-class="hasActiveFilters() ? '' : 'hidden'" |
| @@ -125,8 +125,8 @@ | ||
| 125 | 125 | <div class="flex gap-x-6 items-center"> |
| 126 | 126 | <img x-src="judgeImage" x-alt="judgeName" class="object-cover flex-none size-16 ring-1 ring-gray-900/10 rounded-full grayscale filter"> |
| 127 | 127 | <h1> |
| 128 | <div class="text-gray-500 text-sm/6">Judge <span class="text-gray-700" :text="`#${judgeId}`"></span></div> | |
| 129 | <div class="mt-1 font-semibold text-base text-gray-900" :text="judgeName"></div> | |
| 128 | <div class="text-gray-500 text-sm/6">Judge <span class="text-gray-700">{{ `#${judgeId}` }}</span></div> | |
| 129 | <div class="mt-1 font-semibold text-base text-gray-900">{{ judgeName }}</div> | |
| 130 | 130 | </h1> |
| 131 | 131 | </div> |
| 132 | 132 | <div class="flex gap-x-4 items-center sm:gap-x-6"> |
| @@ -149,8 +149,7 @@ | ||
| 149 | 149 | ? 'bg-white text-gray-900 ring-1 ring-gray-300 hover:bg-gray-50 focus-visible:outline-gray-300' |
| 150 | 150 | : 'bg-gray-900 text-white hover:bg-gray-700 focus-visible:outline-gray-900' |
| 151 | 151 | ]" |
| 152 | :text="followPending ? '…' : (isFollowing ? 'Following' : 'Follow')" | |
| 153 | >Follow</button> | |
| 152 | >{{ followPending ? '…' : (isFollowing ? 'Following' : 'Follow') }}</button> | |
| 154 | 153 | |
| 155 | 154 | <div class="sm:hidden relative"> |
| 156 | 155 | <button type="button" class="block -m-3 p-3" @click="menuOpen.set(!menuOpen())" id="more-menu-button" aria-expanded="false" aria-haspopup="true"> |
| @@ -105,23 +105,21 @@ | ||
| 105 | 105 | <li :for="o in opinions" class="p-5 bg-white border border-gray-200 rounded-lg hover:border-gray-300 hover:shadow-sm transition-all"> |
| 106 | 106 | <div class="flex flex-wrap items-start justify-between gap-3"> |
| 107 | 107 | <div class="min-w-0 flex-1"> |
| 108 | <h3 class="font-semibold text-gray-900 text-base" :text="o.case_name"></h3> | |
| 108 | <h3 class="font-semibold text-gray-900 text-base">{{ o.case_name }}</h3> | |
| 109 | 109 | <div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-gray-500 text-xs"> |
| 110 | <span :if="o.decision_date" :text="formatDate(o.decision_date)"></span> | |
| 110 | <span :if="o.decision_date">{{ formatDate(o.decision_date) }}</span> | |
| 111 | 111 | <span :if="o.decision_date && o.citation" class="text-gray-300">·</span> |
| 112 | <span :if="o.citation" class="font-mono text-gray-600" :text="o.citation"></span> | |
| 112 | <span :if="o.citation" class="font-mono text-gray-600">{{ o.citation }}</span> | |
| 113 | 113 | <span :if="(o.decision_date || o.citation) && o.source_provider !== 'manual'" class="text-gray-300">·</span> |
| 114 | <span :if="o.source_provider !== 'manual'" class="text-gray-500" :text="`via ${o.source_provider}`"></span> | |
| 114 | <span :if="o.source_provider !== 'manual'" class="text-gray-500">{{ `via ${o.source_provider}` }}</span> | |
| 115 | 115 | </div> |
| 116 | 116 | </div> |
| 117 | 117 | <span |
| 118 | 118 | :if="o.outcome_label" |
| 119 | 119 | class="inline-flex items-center px-2.5 py-0.5 font-medium text-xs ring-1 rounded-full capitalize whitespace-nowrap" |
| 120 | x-class="outcomeBadgeClasses(o.outcome_label)" | |
| 121 | :text="o.outcome_label" | |
| 122 | ></span> | |
| 120 | x-class="outcomeBadgeClasses(o.outcome_label)">{{ o.outcome_label }}</span> | |
| 123 | 121 | </div> |
| 124 | <p :if="o.summary" class="mt-3 leading-relaxed text-gray-700 text-sm" :text="o.summary"></p> | |
| 122 | <p :if="o.summary" class="mt-3 leading-relaxed text-gray-700 text-sm">{{ o.summary }}</p> | |
| 125 | 123 | <a |
| 126 | 124 | :if="o.source_url" |
| 127 | 125 | x-href="o.source_url" |
| @@ -136,9 +136,9 @@ | ||
| 136 | 136 | > |
| 137 | 137 | <img x-src="result.photo" class="object-cover flex-none h-10 w-10 ring-1 ring-gray-100 rounded-full grayscale filter" x-alt="result.name"> |
| 138 | 138 | <div class="min-w-0 flex-1"> |
| 139 | <p class="font-medium text-gray-900 text-sm truncate" :text="result.name"></p> | |
| 140 | <p class="text-gray-500 text-xs truncate" :text="result.court.name"></p> | |
| 141 | <p :if="result.location" class="text-gray-400 text-xs truncate" :text="result.location"></p> | |
| 139 | <p class="font-medium text-gray-900 text-sm truncate">{{ result.name }}</p> | |
| 140 | <p class="text-gray-500 text-xs truncate">{{ result.court.name }}</p> | |
| 141 | <p :if="result.location" class="text-gray-400 text-xs truncate">{{ result.location }}</p> | |
| 142 | 142 | </div> |
| 143 | 143 | <svg class="h-4 w-4 text-gray-300 flex-none" viewBox="0 0 20 20" fill="currentColor"> |
| 144 | 144 | <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> |
| @@ -148,7 +148,7 @@ | ||
| 148 | 148 | </div> |
| 149 | 149 | |
| 150 | 150 | <div :if="showEmpty" class="mt-2 p-4 text-center bg-gray-50 border border-gray-200 border-dashed rounded-md"> |
| 151 | <p class="text-gray-600 text-sm">No judges match "<span :text="searchInput()"></span>".</p> | |
| 151 | <p class="text-gray-600 text-sm">No judges match "<span></span>".</p> | |
| 152 | 152 | <p class="mt-1 text-gray-400 text-xs">Try a different name or court.</p> |
| 153 | 153 | </div> |
| 154 | 154 | </div> |
| @@ -52,12 +52,12 @@ | ||
| 52 | 52 | <!-- Header: summary if reviews exist, write-CTA always --> |
| 53 | 53 | <div class="flex items-center justify-between"> |
| 54 | 54 | <div :if="reviewCount() > 0"> |
| 55 | <h2 class="font-semibold text-2xl text-gray-900" :text="averageRating()"></h2> | |
| 55 | <h2 class="font-semibold text-2xl text-gray-900">{{ averageRating() }}</h2> | |
| 56 | 56 | <div class="flex items-center"> |
| 57 | 57 | <div class="flex items-center"> |
| 58 | 58 | <Icon :for="star in stars" name="star" size="20" x-class="star <= Math.round(averageRating()) ? 'text-yellow-400' : 'text-gray-300'" /> |
| 59 | 59 | </div> |
| 60 | <p class="ml-2 text-gray-500 text-sm" :text="`Based on ${reviewCount()} review${reviewCount() === 1 ? '' : 's'}`"></p> | |
| 60 | <p class="ml-2 text-gray-500 text-sm">{{ `Based on ${reviewCount()} review${reviewCount() === 1 ? '' : 's'}` }}</p> | |
| 61 | 61 | </div> |
| 62 | 62 | </div> |
| 63 | 63 | <div :if="reviewCount() === 0"> |
| @@ -90,13 +90,13 @@ | ||
| 90 | 90 | <h3 class="font-semibold text-base text-gray-900">Rating Distribution</h3> |
| 91 | 91 | <div class="mt-4 space-y-3"> |
| 92 | 92 | <div :for="bucket in ratingBuckets()" class="flex items-center"> |
| 93 | <span class="text-gray-500 text-sm" :text="`${bucket.stars} stars`"></span> | |
| 93 | <span class="text-gray-500 text-sm">{{ `${bucket.stars} stars` }}</span> | |
| 94 | 94 | <div class="flex-1 ml-4"> |
| 95 | 95 | <div class="h-2 bg-gray-200 rounded-full"> |
| 96 | 96 | <div class="h-2 bg-yellow-400 rounded-full" x-style="`width: ${bucket.percentage}%`"></div> |
| 97 | 97 | </div> |
| 98 | 98 | </div> |
| 99 | <span class="ml-4 text-gray-500 text-sm" :text="`${bucket.percentage}%`"></span> | |
| 99 | <span class="ml-4 text-gray-500 text-sm">{{ `${bucket.percentage}%` }}</span> | |
| 100 | 100 | </div> |
| 101 | 101 | </div> |
| 102 | 102 | </div> |
| @@ -107,14 +107,14 @@ | ||
| 107 | 107 | <div :for="review in reviews()" class="pb-8 border-b border-gray-200"> |
| 108 | 108 | <div class="flex items-center justify-between"> |
| 109 | 109 | <div class="flex items-center"> |
| 110 | <div class="flex items-center justify-center h-10 w-10 font-semibold text-gray-500 text-sm bg-gray-100 rounded-full" :text="(review.title || 'A').slice(0, 1).toUpperCase()"></div> | |
| 110 | <div class="flex items-center justify-center h-10 w-10 font-semibold text-gray-500 text-sm bg-gray-100 rounded-full">{{ (review.title || 'A').slice(0, 1).toUpperCase() }}</div> | |
| 111 | 111 | <div class="ml-4"> |
| 112 | <h4 class="font-semibold text-gray-900 text-sm" :text="review.title"></h4> | |
| 112 | <h4 class="font-semibold text-gray-900 text-sm">{{ review.title }}</h4> | |
| 113 | 113 | <div class="flex items-center"> |
| 114 | 114 | <div class="flex items-center"> |
| 115 | 115 | <Icon :for="star in stars" name="star" size="16" x-class="star <= review.rating ? 'text-yellow-400' : 'text-gray-300'" /> |
| 116 | 116 | </div> |
| 117 | <span class="ml-2 text-gray-500 text-sm" :text="formatDate(review.created_at)"></span> | |
| 117 | <span class="ml-2 text-gray-500 text-sm">{{ formatDate(review.created_at) }}</span> | |
| 118 | 118 | </div> |
| 119 | 119 | </div> |
| 120 | 120 | </div> |
| @@ -123,10 +123,10 @@ | ||
| 123 | 123 | </button> |
| 124 | 124 | </div> |
| 125 | 125 | <div class="mt-4 text-gray-500 text-sm"> |
| 126 | <p :text="review.content"></p> | |
| 126 | <p>{{ review.content }}</p> | |
| 127 | 127 | </div> |
| 128 | 128 | <div class="flex items-center mt-4 space-x-4"> |
| 129 | <button type="button" class="font-semibold text-gray-500 text-sm hover:text-gray-700" :text="`Helpful (${review.likes ?? 0})`"></button> | |
| 129 | <button type="button" class="font-semibold text-gray-500 text-sm hover:text-gray-700">{{ `Helpful (${review.likes ?? 0})` }}</button> | |
| 130 | 130 | </div> |
| 131 | 131 | </div> |
| 132 | 132 | </div> |
| @@ -116,7 +116,7 @@ | ||
| 116 | 116 | <div x-class="isAuthed() && phase() === 'claimed' ? '' : 'hidden'" class="mt-8 p-6 text-center bg-white border border-amber-200 rounded-2xl shadow-sm"> |
| 117 | 117 | <h2 class="font-serif font-bold text-2xl text-emerald-700">Claim submitted ✓</h2> |
| 118 | 118 | <p class="mt-2 text-gray-700 text-sm"> |
| 119 | Thanks — we've recorded your claim for <span class="font-semibold" :text="claimedName()"></span>. An admin will verify your identity before you can respond to reviews. We'll be in touch. | |
| 119 | Thanks — we've recorded your claim for <span class="font-semibold"></span>. An admin will verify your identity before you can respond to reviews. We'll be in touch. | |
| 120 | 120 | </p> |
| 121 | 121 | <StxLink to="/" class="inline-block mt-5 font-semibold text-gray-900 text-sm hover:underline underline-offset-4">← Back home</StxLink> |
| 122 | 122 | </div> |
| @@ -144,10 +144,10 @@ | ||
| 144 | 144 | @click="pick(j)" |
| 145 | 145 | class="flex w-full items-center gap-3 px-3 py-2 text-left hover:bg-gray-50" |
| 146 | 146 | > |
| 147 | <span class="inline-flex flex-none items-center justify-center h-8 w-8 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full" :text="(j.name || '?').charAt(0)"></span> | |
| 147 | <span class="inline-flex flex-none items-center justify-center h-8 w-8 font-semibold text-amber-700 text-xs bg-amber-50 ring-1 ring-amber-200/60 rounded-full">{{ (j.name || '?').charAt(0) }}</span> | |
| 148 | 148 | <span class="min-w-0"> |
| 149 | <span class="block font-medium text-gray-900 text-sm" :text="j.name"></span> | |
| 150 | <span class="block text-gray-500 text-xs" :text="j.location || (j.court ? j.court.name : '')"></span> | |
| 149 | <span class="block font-medium text-gray-900 text-sm">{{ j.name }}</span> | |
| 150 | <span class="block text-gray-500 text-xs">{{ j.location || (j.court ? j.court.name : '') }}</span> | |
| 151 | 151 | </span> |
| 152 | 152 | </button> |
| 153 | 153 | </li> |
| @@ -156,7 +156,7 @@ | ||
| 156 | 156 | <!-- Selected confirmation + submit --> |
| 157 | 157 | <div x-class="hasSelection() ? '' : 'hidden'" class="mt-4 p-3 bg-amber-50/60 border border-amber-200 rounded-md"> |
| 158 | 158 | <p class="text-gray-700 text-sm"> |
| 159 | Claiming: <span class="font-semibold" :text="selected() ? selected().name : ''"></span> | |
| 159 | Claiming: <span class="font-semibold">{{ selected() ? selected().name : '' }}</span> | |
| 160 | 160 | </p> |
| 161 | 161 | <button |
| 162 | 162 | type="button" |
| @@ -165,16 +165,14 @@ | ||
| 165 | 165 | class="mt-3 inline-flex items-center justify-center px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50" |
| 166 | 166 | > |
| 167 | 167 | <svg :if="submitting()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 168 | <span :text="submitting() ? 'Submitting…' : 'Submit claim for verification'"></span> | |
| 168 | <span>{{ submitting() ? 'Submitting…' : 'Submit claim for verification' }}</span> | |
| 169 | 169 | </button> |
| 170 | 170 | </div> |
| 171 | 171 | |
| 172 | 172 | <div |
| 173 | 173 | role="alert" |
| 174 | 174 | x-class="errorMsg() ? '' : 'hidden'" |
| 175 | class="mt-3 px-3 py-2 text-red-800 text-xs bg-red-50 border border-red-200 rounded-md" | |
| 176 | :text="errorMsg()" | |
| 177 | ></div> | |
| 175 | class="mt-3 px-3 py-2 text-red-800 text-xs bg-red-50 border border-red-200 rounded-md">{{ errorMsg() }}</div> | |
| 178 | 176 | |
| 179 | 177 | <p class="mt-4 text-gray-400 text-xs leading-relaxed"> |
| 180 | 178 | Can't find your profile? It may not be in our directory yet. Contact us and we'll add it. |
| @@ -15,8 +15,8 @@ | ||
| 15 | 15 | <div :for="judge in trendingJudges" class="flex items-center space-x-3"> |
| 16 | 16 | <img class="h-8 w-8 rounded-full grayscale filter" x-src="judge.photo" x-alt="judge.name" /> |
| 17 | 17 | <div class="flex-1 min-w-0"> |
| 18 | <p class="font-medium text-gray-900 text-sm" :text="judge.name"></p> | |
| 19 | <p class="text-gray-500 text-sm" :text="judge.court"></p> | |
| 18 | <p class="font-medium text-gray-900 text-sm">{{ judge.name }}</p> | |
| 19 | <p class="text-gray-500 text-sm">{{ judge.court }}</p> | |
| 20 | 20 | </div> |
| 21 | 21 | <button type="button" class="font-semibold text-gray-900 text-xs hover:underline">Follow</button> |
| 22 | 22 | </div> |
| @@ -36,15 +36,15 @@ | ||
| 36 | 36 | <div class="flex relative space-x-3"> |
| 37 | 37 | <div> |
| 38 | 38 | <span class="flex items-center justify-center h-8 w-8 bg-gray-400 ring-8 ring-white rounded-full"> |
| 39 | <span class="h-5 w-5 text-white" aria-hidden="true" :text="activity.icon"></span> | |
| 39 | <span class="h-5 w-5 text-white" aria-hidden="true">{{ activity.icon }}</span> | |
| 40 | 40 | </span> |
| 41 | 41 | </div> |
| 42 | 42 | <div class="flex flex-1 justify-between pt-1.5 space-x-4 min-w-0"> |
| 43 | 43 | <div> |
| 44 | <p class="text-gray-500 text-sm" :text="activity.content"></p> | |
| 44 | <p class="text-gray-500 text-sm">{{ activity.content }}</p> | |
| 45 | 45 | </div> |
| 46 | 46 | <div class="text-gray-500 text-right text-sm whitespace-nowrap"> |
| 47 | <time :datetime="activity.dateTime" :text="activity.date"></time> | |
| 47 | <time :datetime="activity.dateTime">{{ activity.date }}</time> | |
| 48 | 48 | </div> |
| 49 | 49 | </div> |
| 50 | 50 | </div> |
| @@ -151,7 +151,7 @@ | ||
| 151 | 151 | x-class="statusFilter === 'all' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 152 | 152 | > |
| 153 | 153 | All |
| 154 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'all' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'" :text="counts.all"></span> | |
| 154 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'all' ? 'bg-white/15 text-white' : 'bg-gray-100 text-gray-600'">{{ counts.all }}</span> | |
| 155 | 155 | </button> |
| 156 | 156 | <button |
| 157 | 157 | type="button" |
| @@ -161,7 +161,7 @@ | ||
| 161 | 161 | x-class="statusFilter === 'pending' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 162 | 162 | > |
| 163 | 163 | Pending |
| 164 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'pending' ? 'bg-white/15 text-white' : 'bg-amber-50 text-amber-700'" :text="counts.pending"></span> | |
| 164 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'pending' ? 'bg-white/15 text-white' : 'bg-amber-50 text-amber-700'">{{ counts.pending }}</span> | |
| 165 | 165 | </button> |
| 166 | 166 | <button |
| 167 | 167 | type="button" |
| @@ -171,7 +171,7 @@ | ||
| 171 | 171 | x-class="statusFilter === 'published' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 172 | 172 | > |
| 173 | 173 | Published |
| 174 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'published' ? 'bg-white/15 text-white' : 'bg-emerald-50 text-emerald-700'" :text="counts.published"></span> | |
| 174 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'published' ? 'bg-white/15 text-white' : 'bg-emerald-50 text-emerald-700'">{{ counts.published }}</span> | |
| 175 | 175 | </button> |
| 176 | 176 | <button |
| 177 | 177 | type="button" |
| @@ -181,7 +181,7 @@ | ||
| 181 | 181 | x-class="statusFilter === 'rejected' ? 'bg-gray-900 text-white border-gray-900' : 'bg-white text-gray-700 border-gray-200 hover:border-gray-300'" |
| 182 | 182 | > |
| 183 | 183 | Declined |
| 184 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'rejected' ? 'bg-white/15 text-white' : 'bg-red-50 text-red-700'" :text="counts.rejected"></span> | |
| 184 | <span class="px-1.5 py-0.5 tabular-nums text-xs rounded" x-class="statusFilter === 'rejected' ? 'bg-white/15 text-white' : 'bg-red-50 text-red-700'">{{ counts.rejected }}</span> | |
| 185 | 185 | </button> |
| 186 | 186 | </div> |
| 187 | 187 | |
| @@ -215,8 +215,8 @@ | ||
| 215 | 215 | initial load to avoid the "no reviews" flash before the |
| 216 | 216 | first fetch returns. --> |
| 217 | 217 | <div :if="profileStore.loaded && filtered.length === 0" class="mt-10 p-10 text-center bg-white border border-gray-200 rounded-lg"> |
| 218 | <p class="font-semibold text-gray-900 text-base" :text="emptyTitle"></p> | |
| 219 | <p class="mt-2 mx-auto max-w-md text-gray-500 text-sm" :text="emptyHint"></p> | |
| 218 | <p class="font-semibold text-gray-900 text-base">{{ emptyTitle }}</p> | |
| 219 | <p class="mt-2 mx-auto max-w-md text-gray-500 text-sm">{{ emptyHint }}</p> | |
| 220 | 220 | <StxLink to="/review" class="inline-flex items-center mt-6 px-4 py-2 font-medium text-white text-sm bg-gray-900 rounded-md hover:bg-gray-800"> |
| 221 | 221 | Write a review |
| 222 | 222 | </StxLink> |
| @@ -231,14 +231,10 @@ | ||
| 231 | 231 | <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1"> |
| 232 | 232 | <StxLink |
| 233 | 233 | x-to="`/article/${r.id}`" |
| 234 | class="font-semibold text-gray-900 text-base hover:underline underline-offset-2 decoration-gray-300" | |
| 235 | :text="r.title" | |
| 236 | ></StxLink> | |
| 234 | class="font-semibold text-gray-900 text-base hover:underline underline-offset-2 decoration-gray-300">{{ r.title }}</StxLink> | |
| 237 | 235 | <span |
| 238 | 236 | class="inline-flex items-center px-2 py-0.5 font-medium text-xs ring-1 rounded-full" |
| 239 | x-class="statusBadgeClasses(r.status)" | |
| 240 | :text="statusLabel(r.status)" | |
| 241 | ></span> | |
| 237 | x-class="statusBadgeClasses(r.status)">{{ statusLabel(r.status) }}</span> | |
| 242 | 238 | </div> |
| 243 | 239 | |
| 244 | 240 | <div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-gray-500 text-xs"> |
| @@ -246,19 +242,17 @@ | ||
| 246 | 242 | Review of |
| 247 | 243 | <StxLink |
| 248 | 244 | x-to="`/judges/${r.judge.id}/profile`" |
| 249 | class="text-gray-700 hover:underline underline-offset-2" | |
| 250 | :text="r.judge.name" | |
| 251 | ></StxLink> | |
| 245 | class="text-gray-700 hover:underline underline-offset-2">{{ r.judge.name }}</StxLink> | |
| 252 | 246 | </span> |
| 253 | 247 | <span class="text-gray-300">·</span> |
| 254 | <span :text="formatDate(r.created_at)"></span> | |
| 248 | <span>{{ formatDate(r.created_at) }}</span> | |
| 255 | 249 | <span class="text-gray-300">·</span> |
| 256 | <span class="inline-flex items-center gap-1 tabular-nums" :text="`${r.rating}/5`"></span> | |
| 250 | <span class="inline-flex items-center gap-1 tabular-nums">{{ `${r.rating}/5` }}</span> | |
| 257 | 251 | <span :if="r.likes && r.likes > 0" class="text-gray-300">·</span> |
| 258 | <span :if="r.likes && r.likes > 0" class="inline-flex items-center gap-1" :text="`${r.likes} ${r.likes === 1 ? 'reaction' : 'reactions'}`"></span> | |
| 252 | <span :if="r.likes && r.likes > 0" class="inline-flex items-center gap-1">{{ `${r.likes} ${r.likes === 1 ? 'reaction' : 'reactions'}` }}</span> | |
| 259 | 253 | </div> |
| 260 | 254 | |
| 261 | <p class="mt-3 text-gray-600 text-sm line-clamp-2" :text="snippet(r.content)"></p> | |
| 255 | <p class="mt-3 text-gray-600 text-sm line-clamp-2">{{ snippet(r.content) }}</p> | |
| 262 | 256 | |
| 263 | 257 | <div class="flex flex-wrap items-center gap-x-4 mt-4 pt-3 border-t border-gray-100 text-sm"> |
| 264 | 258 | <StxLink x-to="`/article/${r.id}`" class="font-medium text-gray-900 hover:underline underline-offset-2"> |
| @@ -83,7 +83,7 @@ | ||
| 83 | 83 | <div> |
| 84 | 84 | <h1 class="font-bold text-2xl text-gray-900">Notifications</h1> |
| 85 | 85 | <p class="mt-1 text-gray-500 text-sm"> |
| 86 | <span :text="store.unreadCount()"></span> unread | |
| 86 | <span>{{ store.unreadCount() }}</span> unread | |
| 87 | 87 | </p> |
| 88 | 88 | </div> |
| 89 | 89 | <button |
| @@ -125,8 +125,8 @@ | ||
| 125 | 125 | <path d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /> |
| 126 | 126 | </svg> |
| 127 | 127 | </div> |
| 128 | <p class="font-semibold text-gray-900 text-sm" :text="emptyTitle()"></p> | |
| 129 | <p class="mt-1 max-w-md mx-auto text-gray-500 text-xs leading-relaxed" :text="emptyHint()"></p> | |
| 128 | <p class="font-semibold text-gray-900 text-sm">{{ emptyTitle() }}</p> | |
| 129 | <p class="mt-1 max-w-md mx-auto text-gray-500 text-xs leading-relaxed">{{ emptyHint() }}</p> | |
| 130 | 130 | </div> |
| 131 | 131 | <ul x-class="store.items().length > 0 ? 'divide-gray-100 divide-y' : 'hidden'"> |
| 132 | 132 | <li :for="n in store.items" class="hover:bg-gray-50"> |
| @@ -137,9 +137,9 @@ | ||
| 137 | 137 | > |
| 138 | 138 | <span class="mt-1 inline-flex h-2.5 w-2.5 flex-shrink-0 rounded-full" x-class="n.unread ? 'bg-blue-500' : 'bg-transparent'"></span> |
| 139 | 139 | <div class="flex-1 min-w-0"> |
| 140 | <p class="font-medium text-gray-900 text-sm" :text="labelFor(n)"></p> | |
| 141 | <p class="mt-1 text-gray-500 text-xs" :text="detailFor(n)"></p> | |
| 142 | <p class="mt-1 text-gray-400 text-xs" :text="relativeTime(n.created_at)"></p> | |
| 140 | <p class="font-medium text-gray-900 text-sm">{{ labelFor(n) }}</p> | |
| 141 | <p class="mt-1 text-gray-500 text-xs">{{ detailFor(n) }}</p> | |
| 142 | <p class="mt-1 text-gray-400 text-xs">{{ relativeTime(n.created_at) }}</p> | |
| 143 | 143 | </div> |
| 144 | 144 | </button> |
| 145 | 145 | </li> |
| @@ -43,7 +43,7 @@ | ||
| 43 | 43 | <p class="mt-4 text-gray-600 text-sm/6">Are you a clerk? This is for you.</p> |
| 44 | 44 | <p class="flex gap-x-1 items-baseline mt-6"> |
| 45 | 45 | <span class="font-semibold text-4xl text-gray-800 tracking-tight">$0</span> |
| 46 | <span class="font-semibold text-gray-600 text-sm/6">/<span x-text="pricingPeriod() === 'monthly' ? 'month' : 'year'"></span></span> | |
| 46 | <span class="font-semibold text-gray-600 text-sm/6">/<span>{{ pricingPeriod() === 'monthly' ? 'month' : 'year' }}</span></span> | |
| 47 | 47 | </p> |
| 48 | 48 | <StxLink to="/register" aria-describedby="tier-free" |
| 49 | 49 | class="block mt-6 px-2.5 py-1.5 font-semibold text-center text-gray-600 text-sm/6 ring-1 ring-gray-200 ring-inset rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-gray-600 focus-visible:outline-offset-2 hover:ring-gray-300">Get |
| @@ -63,8 +63,8 @@ | ||
| 63 | 63 | <h3 id="tier-hobby" class="font-semibold text-gray-600 text-lg/8">Professional</h3> |
| 64 | 64 | <p class="mt-4 text-gray-600 text-sm/6">For anyone curious about judge reviews.</p> |
| 65 | 65 | <p class="flex gap-x-1 items-baseline mt-6"> |
| 66 | <span class="font-semibold text-4xl text-gray-800 tracking-tight">$<span x-text="pricingPeriod() === 'monthly' ? '10' : '60'"></span></span> | |
| 67 | <span class="font-semibold text-gray-600 text-sm/6">/<span x-text="pricingPeriod() === 'monthly' ? 'month' : 'year'"></span></span> | |
| 66 | <span class="font-semibold text-4xl text-gray-800 tracking-tight">$<span>{{ pricingPeriod() === 'monthly' ? '10' : '60' }}</span></span> | |
| 67 | <span class="font-semibold text-gray-600 text-sm/6">/<span>{{ pricingPeriod() === 'monthly' ? 'month' : 'year' }}</span></span> | |
| 68 | 68 | </p> |
| 69 | 69 | <StxLink to="/register" aria-describedby="tier-hobby" |
| 70 | 70 | class="block mt-6 px-2.5 py-1.5 font-semibold text-center text-sm/6 text-white bg-gray-600 hover:bg-gray-500 rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-gray-600 focus-visible:outline-offset-2 shadow-sm">Get |
| @@ -100,9 +100,9 @@ | ||
| 100 | 100 | <div class="flex gap-4 items-center"> |
| 101 | 101 | <img class="object-cover h-24 w-24 ring-2 ring-white/20 rounded-full" x-src="avatarUrl" x-alt="displayName" /> |
| 102 | 102 | <div> |
| 103 | <h1 class="font-bold text-2xl text-white" :text="displayName"></h1> | |
| 104 | <p class="text-gray-400 text-sm" :text="userEmail"></p> | |
| 105 | <p class="mt-1 text-gray-400 text-xs" :text="memberSince"></p> | |
| 103 | <h1 class="font-bold text-2xl text-white">{{ displayName }}</h1> | |
| 104 | <p class="text-gray-400 text-sm">{{ userEmail }}</p> | |
| 105 | <p class="mt-1 text-gray-400 text-xs">{{ memberSince }}</p> | |
| 106 | 106 | </div> |
| 107 | 107 | </div> |
| 108 | 108 | <div class="flex flex-wrap gap-2"> |
| @@ -117,19 +117,19 @@ | ||
| 117 | 117 | <div class="grid gap-4 grid-cols-2 mt-8 lg:grid-cols-4"> |
| 118 | 118 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 119 | 119 | <p class="text-gray-500 text-sm">Total Reviews</p> |
| 120 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="profileStore.totalReviews"></p> | |
| 120 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ profileStore.totalReviews }}</p> | |
| 121 | 121 | </div> |
| 122 | 122 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 123 | 123 | <p class="text-gray-500 text-sm">Judges Reviewed</p> |
| 124 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="profileStore.judgesReviewed"></p> | |
| 124 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ profileStore.judgesReviewed }}</p> | |
| 125 | 125 | </div> |
| 126 | 126 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 127 | 127 | <p class="text-gray-500 text-sm">Average Rating</p> |
| 128 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="profileStore.averageRating || '—'"></p> | |
| 128 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ profileStore.averageRating || '—' }}</p> | |
| 129 | 129 | </div> |
| 130 | 130 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 131 | 131 | <p class="text-gray-500 text-sm">Total Likes</p> |
| 132 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="profileStore.totalLikes"></p> | |
| 132 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ profileStore.totalLikes }}</p> | |
| 133 | 133 | </div> |
| 134 | 134 | </div> |
| 135 | 135 | |
| @@ -163,15 +163,15 @@ | ||
| 163 | 163 | <div class="flex-none h-2 w-2 mt-2 bg-gray-900 rounded-full"></div> |
| 164 | 164 | <div class="flex-1 min-w-0"> |
| 165 | 165 | <div class="flex flex-wrap items-baseline gap-x-2"> |
| 166 | <StxLink x-to="`/article/${review.id}`" class="font-medium text-gray-900 text-sm hover:underline" :text="review.title"></StxLink> | |
| 166 | <StxLink x-to="`/article/${review.id}`" class="font-medium text-gray-900 text-sm hover:underline">{{ review.title }}</StxLink> | |
| 167 | 167 | <span :if="review.judge" class="text-gray-500 text-xs"> |
| 168 | 168 | on |
| 169 | <StxLink x-to="`/judges/${review.judge.id}/profile`" class="hover:underline" :text="review.judge.name"></StxLink> | |
| 169 | <StxLink x-to="`/judges/${review.judge.id}/profile`" class="hover:underline">{{ review.judge.name }}</StxLink> | |
| 170 | 170 | </span> |
| 171 | 171 | </div> |
| 172 | <p class="mt-0.5 text-gray-500 text-sm" :text="snippet(review.content)"></p> | |
| 172 | <p class="mt-0.5 text-gray-500 text-sm">{{ snippet(review.content) }}</p> | |
| 173 | 173 | <div class="flex items-center gap-3 mt-1"> |
| 174 | <span class="text-gray-400 text-xs" :text="relativeTime(review.created_at)"></span> | |
| 174 | <span class="text-gray-400 text-xs">{{ relativeTime(review.created_at) }}</span> | |
| 175 | 175 | <span :if="review.status === 'pending'" class="px-1.5 py-0.5 font-medium text-yellow-700 text-xs bg-yellow-50 ring-1 ring-yellow-200 rounded">Pending review</span> |
| 176 | 176 | </div> |
| 177 | 177 | </div> |
| @@ -186,8 +186,8 @@ | ||
| 186 | 186 | <li :for="j in suggestedJudges" class="flex gap-3 items-center"> |
| 187 | 187 | <img class="object-cover flex-none h-10 w-10 rounded-full grayscale filter" x-src="j.image_url || j.photo" x-alt="j.name" /> |
| 188 | 188 | <div class="flex-1 min-w-0"> |
| 189 | <p class="font-medium text-gray-900 text-sm truncate" :text="j.name"></p> | |
| 190 | <p class="text-gray-500 text-xs truncate" :text="(j.court && j.court.name) || j.court || ''"></p> | |
| 189 | <p class="font-medium text-gray-900 text-sm truncate">{{ j.name }}</p> | |
| 190 | <p class="text-gray-500 text-xs truncate">{{ (j.court && j.court.name) || j.court || '' }}</p> | |
| 191 | 191 | </div> |
| 192 | 192 | <StxLink x-to="`/review/${j.id}`" class="flex-none px-2.5 py-1 font-medium text-gray-900 text-xs bg-gray-100 hover:bg-gray-200 rounded-md transition-colors">Review</StxLink> |
| 193 | 193 | </li> |
| @@ -375,7 +375,7 @@ | ||
| 375 | 375 | </div> |
| 376 | 376 | <h2 class="mt-5 font-semibold text-xl text-gray-900">Thanks — your review is in.</h2> |
| 377 | 377 | <p class="mt-2 leading-7 text-gray-600 text-sm max-w-md mx-auto"> |
| 378 | Your review of <span class="font-medium text-gray-900" :text="submitted().judgeName"></span> | |
| 378 | Your review of <span class="font-medium text-gray-900">{{ submitted().judgeName }}</span> | |
| 379 | 379 | is pending moderation. We typically publish within a day. You can see your reviews any time |
| 380 | 380 | from your profile. |
| 381 | 381 | </p> |
| @@ -393,7 +393,7 @@ | ||
| 393 | 393 | <div :if="submitted().reviewId" class="mt-8 pt-6 border-t border-gray-100 text-left max-w-md mx-auto"> |
| 394 | 394 | <div class="flex items-baseline justify-between mb-3"> |
| 395 | 395 | <h3 class="font-semibold text-gray-900 text-sm">Attach photos (optional)</h3> |
| 396 | <span class="text-gray-500 text-xs" :text="`${photoCount} / 4 attached`"></span> | |
| 396 | <span class="text-gray-500 text-xs">{{ `${photoCount} / 4 attached` }}</span> | |
| 397 | 397 | </div> |
| 398 | 398 | <p class="mb-3 text-gray-500 text-xs leading-relaxed">JPEG, PNG, or WebP up to 8MB each. Camera metadata (including GPS) is stripped at upload — your photos won't reveal where you were when you took them.</p> |
| 399 | 399 | |
| @@ -418,12 +418,10 @@ | ||
| 418 | 418 | <div |
| 419 | 419 | role="alert" |
| 420 | 420 | class="mt-3 px-3 py-2 text-red-800 text-xs bg-red-50 border border-red-200 rounded-md" |
| 421 | x-class="photoError() ? '' : 'hidden'" | |
| 422 | :text="photoError()" | |
| 423 | ></div> | |
| 421 | x-class="photoError() ? '' : 'hidden'">{{ photoError() }}</div> | |
| 424 | 422 | |
| 425 | 423 | <p :if="photoUploading" class="mt-3 text-gray-500 text-xs">Uploading and resizing — this can take a few seconds per photo.</p> |
| 426 | <p :if="!photoUploading() && photoCount() > 0" class="mt-3 text-emerald-700 text-xs" :text="`${photoCount} ${photoCount() === 1 ? 'photo' : 'photos'} attached. You can add more or close this page — they'll appear on your review once moderation publishes it.`"></p> | |
| 424 | <p :if="!photoUploading() && photoCount() > 0" class="mt-3 text-emerald-700 text-xs">{{ `${photoCount} ${photoCount() === 1 ? 'photo' : 'photos'} attached. You can add more or close this page — they'll appear on your review once moderation publishes it.` }}</p> | |
| 427 | 425 | </div> |
| 428 | 426 | </div> |
| 429 | 427 | |
| @@ -441,9 +439,9 @@ | ||
| 441 | 439 | <div class="flex gap-4 items-center"> |
| 442 | 440 | <img class="object-cover h-16 w-16 ring-2 ring-gray-100 rounded-full grayscale filter" x-src="judge().photo" x-alt="judge().name" /> |
| 443 | 441 | <div class="min-w-0"> |
| 444 | <h3 class="font-semibold text-gray-900 text-lg" :text="judge().name"></h3> | |
| 445 | <p class="text-gray-500 text-sm" :text="judge().court"></p> | |
| 446 | <p :if="judge().department" class="text-gray-400 text-xs" :text="judge().department"></p> | |
| 442 | <h3 class="font-semibold text-gray-900 text-lg">{{ judge().name }}</h3> | |
| 443 | <p class="text-gray-500 text-sm">{{ judge().court }}</p> | |
| 444 | <p :if="judge().department" class="text-gray-400 text-xs">{{ judge().department }}</p> | |
| 447 | 445 | </div> |
| 448 | 446 | </div> |
| 449 | 447 | <button |
| @@ -500,7 +498,7 @@ | ||
| 500 | 498 | <Icon name="star" size="24" /> |
| 501 | 499 | </button> |
| 502 | 500 | </div> |
| 503 | <span class="ml-2 w-24 text-gray-500 text-sm" :text="fairness() ? getRatingText(fairness()) : ''"></span> | |
| 501 | <span class="ml-2 w-24 text-gray-500 text-sm">{{ fairness() ? getRatingText(fairness()) : '' }}</span> | |
| 504 | 502 | </div> |
| 505 | 503 | </div> |
| 506 | 504 | |
| @@ -521,7 +519,7 @@ | ||
| 521 | 519 | <Icon name="star" size="24" /> |
| 522 | 520 | </button> |
| 523 | 521 | </div> |
| 524 | <span class="ml-2 w-24 text-gray-500 text-sm" :text="knowledge() ? getRatingText(knowledge()) : ''"></span> | |
| 522 | <span class="ml-2 w-24 text-gray-500 text-sm">{{ knowledge() ? getRatingText(knowledge()) : '' }}</span> | |
| 525 | 523 | </div> |
| 526 | 524 | </div> |
| 527 | 525 | |
| @@ -542,7 +540,7 @@ | ||
| 542 | 540 | <Icon name="star" size="24" /> |
| 543 | 541 | </button> |
| 544 | 542 | </div> |
| 545 | <span class="ml-2 w-24 text-gray-500 text-sm" :text="demeanor() ? getRatingText(demeanor()) : ''"></span> | |
| 543 | <span class="ml-2 w-24 text-gray-500 text-sm">{{ demeanor() ? getRatingText(demeanor()) : '' }}</span> | |
| 546 | 544 | </div> |
| 547 | 545 | </div> |
| 548 | 546 | </div> |
| @@ -582,7 +580,7 @@ | ||
| 582 | 580 | <Icon name="star" size="32" /> |
| 583 | 581 | </button> |
| 584 | 582 | </div> |
| 585 | <span class="ml-3 font-medium text-gray-700 text-sm" :text="ratingText()"></span> | |
| 583 | <span class="ml-3 font-medium text-gray-700 text-sm">{{ ratingText() }}</span> | |
| 586 | 584 | </div> |
| 587 | 585 | </div> |
| 588 | 586 | |
| @@ -616,9 +614,7 @@ | ||
| 616 | 614 | role="alert" |
| 617 | 615 | aria-live="polite" |
| 618 | 616 | class="mb-4 p-3 text-red-700 text-sm bg-red-50 border border-red-200 rounded-md" |
| 619 | x-class="submitError() ? '' : 'hidden'" | |
| 620 | :text="submitError()" | |
| 621 | ></div> | |
| 617 | x-class="submitError() ? '' : 'hidden'">{{ submitError() }}</div> | |
| 622 | 618 | |
| 623 | 619 | <!-- Anonymity toggle (bench-review#36). Default OFF — anonymity |
| 624 | 620 | is an explicit opt-in. Surfaces above the action bar so the |
| @@ -663,7 +659,7 @@ | ||
| 663 | 659 | ? 'bg-gray-900 hover:bg-gray-700' |
| 664 | 660 | : 'bg-gray-300 cursor-not-allowed' |
| 665 | 661 | ]" |
| 666 | ><svg :if="reviewsStore.submitting()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg><span :text="reviewsStore.submitting() ? 'Submitting…' : 'Submit Review'"></span></button> | |
| 662 | ><svg :if="reviewsStore.submitting()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg><span>{{ reviewsStore.submitting() ? 'Submitting…' : 'Submit Review' }}</span></button> | |
| 667 | 663 | </div> |
| 668 | 664 | </div> |
| 669 | 665 | |
| @@ -208,9 +208,7 @@ | ||
| 208 | 208 | <div |
| 209 | 209 | role="alert" |
| 210 | 210 | class="mb-4 px-3 py-2 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 211 | x-class="likeError() ? '' : 'hidden'" | |
| 212 | :text="likeError()" | |
| 213 | ></div> | |
| 211 | x-class="likeError() ? '' : 'hidden'">{{ likeError() }}</div> | |
| 214 | 212 | <!-- Loading skeleton (bench-review#35). Renders three placeholder |
| 215 | 213 | review cards matching the eventual layout (avatar circle + name |
| 216 | 214 | line + meta line + 3-line body + footer chip row) so the feed |
| @@ -275,25 +273,25 @@ | ||
| 275 | 273 | </StxLink> |
| 276 | 274 | <div class="ml-2"> |
| 277 | 275 | <p class="font-medium text-gray-900 text-sm"> |
| 278 | <span :if="review.author.id === 0" :text="review.author.name"></span> | |
| 279 | <StxLink :if="review.author.id !== 0" x-to="`/user/${review.author.id}`" class="hover:underline" :text="review.author.name"></StxLink> | |
| 276 | <span :if="review.author.id === 0">{{ review.author.name }}</span> | |
| 277 | <StxLink :if="review.author.id !== 0" x-to="`/user/${review.author.id}`" class="hover:underline">{{ review.author.name }}</StxLink> | |
| 280 | 278 | </p> |
| 281 | 279 | <p class="text-gray-500 text-xs">reviewed</p> |
| 282 | 280 | </div> |
| 283 | 281 | </div> |
| 284 | <span class="text-gray-600 text-sm" :text="review.date"></span> | |
| 282 | <span class="text-gray-600 text-sm">{{ review.date }}</span> | |
| 285 | 283 | </div> |
| 286 | 284 | |
| 287 | 285 | <!-- Title + content --> |
| 288 | 286 | <div class="mt-3"> |
| 289 | <h3 class="mb-2 font-bold text-gray-900 text-xl" :text="review.title"></h3> | |
| 287 | <h3 class="mb-2 font-bold text-gray-900 text-xl">{{ review.title }}</h3> | |
| 290 | 288 | <p class="leading-relaxed line-clamp-3 text-base text-gray-700" x-html="review.content"></p> |
| 291 | 289 | </div> |
| 292 | 290 | |
| 293 | 291 | <!-- Judge name + rating + actions --> |
| 294 | 292 | <div class="flex items-center justify-between mt-4"> |
| 295 | 293 | <div class="flex items-center space-x-3"> |
| 296 | <StxLink x-to="`/judges/${review.judge.id}/profile`" class="font-semibold text-base text-gray-900 hover:underline" :text="review.judge.name"></StxLink> | |
| 294 | <StxLink x-to="`/judges/${review.judge.id}/profile`" class="font-semibold text-base text-gray-900 hover:underline">{{ review.judge.name }}</StxLink> | |
| 297 | 295 | <div class="flex items-center"> |
| 298 | 296 | <Icon :for="star in stars" name="star" size="16" x-class="star <= review.rating ? 'text-yellow-400' : 'text-gray-300'" /> |
| 299 | 297 | </div> |
| @@ -314,7 +312,7 @@ | ||
| 314 | 312 | <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="mr-1.5 transition-colors"> |
| 315 | 313 | <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> |
| 316 | 314 | </svg> |
| 317 | <span class="tabular-nums" :text="review.likes"></span> | |
| 315 | <span class="tabular-nums">{{ review.likes }}</span> | |
| 318 | 316 | </button> |
| 319 | 317 | <!-- Author: read-only count. Still shows the reaction |
| 320 | 318 | total — it's useful feedback for the writer — but no |
| @@ -328,7 +326,7 @@ | ||
| 328 | 326 | <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="mr-1.5"> |
| 329 | 327 | <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> |
| 330 | 328 | </svg> |
| 331 | <span class="tabular-nums" :text="review.likes"></span> | |
| 329 | <span class="tabular-nums">{{ review.likes }}</span> | |
| 332 | 330 | </span> |
| 333 | 331 | <StxLink x-to="`/article/${review.id}`" class="text-gray-600 text-sm hover:text-gray-500 hover:underline"> |
| 334 | 332 | Read more |
| @@ -91,10 +91,10 @@ | ||
| 91 | 91 | <div class="flex items-center justify-between mt-6"> |
| 92 | 92 | <div> |
| 93 | 93 | <h1 class="font-semibold text-2xl text-gray-900"> |
| 94 | <span :if="search.query" :text="`Results for "${search.query}"`"></span> | |
| 94 | <span :if="search.query">{{ `Results for "${search.query}"` }}</span> | |
| 95 | 95 | <span :if="!search.query">All results</span> |
| 96 | 96 | </h1> |
| 97 | <p class="mt-1 text-gray-600 text-sm" :text="totalResults + ' results found'"></p> | |
| 97 | <p class="mt-1 text-gray-600 text-sm">{{ totalResults + ' results found' }}</p> | |
| 98 | 98 | </div> |
| 99 | 99 | </div> |
| 100 | 100 | |
| @@ -106,7 +106,7 @@ | ||
| 106 | 106 | class="px-1 py-3 font-medium text-sm whitespace-nowrap border-b-2" |
| 107 | 107 | x-class="search.tab === 'all' ? 'border-gray-600 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'" |
| 108 | 108 | > |
| 109 | All <span class="ml-1 text-gray-400" :text="`(${totalResults})`"></span> | |
| 109 | All <span class="ml-1 text-gray-400">{{ `(${totalResults})` }}</span> | |
| 110 | 110 | </button> |
| 111 | 111 | <button |
| 112 | 112 | type="button" |
| @@ -114,7 +114,7 @@ | ||
| 114 | 114 | class="px-1 py-3 font-medium text-sm whitespace-nowrap border-b-2" |
| 115 | 115 | x-class="search.tab === 'judges' ? 'border-gray-600 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'" |
| 116 | 116 | > |
| 117 | Judges <span class="ml-1 text-gray-400" :text="`(${filteredJudges.length})`"></span> | |
| 117 | Judges <span class="ml-1 text-gray-400">{{ `(${filteredJudges.length})` }}</span> | |
| 118 | 118 | </button> |
| 119 | 119 | <button |
| 120 | 120 | type="button" |
| @@ -122,7 +122,7 @@ | ||
| 122 | 122 | class="px-1 py-3 font-medium text-sm whitespace-nowrap border-b-2" |
| 123 | 123 | x-class="search.tab === 'courts' ? 'border-gray-600 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'" |
| 124 | 124 | > |
| 125 | Courts <span class="ml-1 text-gray-400" :text="`(${filteredCourts.length})`"></span> | |
| 125 | Courts <span class="ml-1 text-gray-400">{{ `(${filteredCourts.length})` }}</span> | |
| 126 | 126 | </button> |
| 127 | 127 | </nav> |
| 128 | 128 | </div> |
| @@ -133,7 +133,7 @@ | ||
| 133 | 133 | <svg class="mx-auto h-12 w-12 text-gray-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> |
| 134 | 134 | <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /> |
| 135 | 135 | </svg> |
| 136 | <p class="mt-4 font-medium text-gray-900" :text="`No results for "${search.query}"`"></p> | |
| 136 | <p class="mt-4 font-medium text-gray-900">{{ `No results for "${search.query}"` }}</p> | |
| 137 | 137 | <p class="mt-1 text-gray-500 text-sm">Try a different name, court, or location.</p> |
| 138 | 138 | <div class="flex gap-3 items-center justify-center mt-5"> |
| 139 | 139 | <StxLink to="/judges" class="inline-flex items-center px-4 py-2 font-medium text-white text-sm bg-gray-800 rounded-lg hover:bg-gray-700 transition-colors">Browse judges</StxLink> |
| @@ -155,13 +155,13 @@ | ||
| 155 | 155 | <StxLink x-to="`/judges/${judge.id}/profile`" class="flex flex-1 gap-x-4 items-center min-w-0"> |
| 156 | 156 | <img class="object-cover flex-none h-12 w-12 rounded-full grayscale filter" x-src="judge.photo" x-alt="judge.name"> |
| 157 | 157 | <div class="flex-auto min-w-0"> |
| 158 | <p class="font-semibold text-gray-900 text-sm hover:underline" :text="judge.name"></p> | |
| 159 | <p class="mt-0.5 text-gray-500 text-xs" :text="`${judge.court.name} · ${judge.location}`"></p> | |
| 158 | <p class="font-semibold text-gray-900 text-sm hover:underline">{{ judge.name }}</p> | |
| 159 | <p class="mt-0.5 text-gray-500 text-xs">{{ `${judge.court.name} · ${judge.location}` }}</p> | |
| 160 | 160 | </div> |
| 161 | 161 | </StxLink> |
| 162 | 162 | <div class="flex gap-x-2 items-center"> |
| 163 | 163 | <Icon :for="star in stars" name="star" size="16" x-class="star <= judge.rating ? 'text-yellow-400' : 'text-gray-200'" /> |
| 164 | <span class="ml-1 text-gray-500 text-xs" :text="`(${judge.reviewCount})`"></span> | |
| 164 | <span class="ml-1 text-gray-500 text-xs">{{ `(${judge.reviewCount})` }}</span> | |
| 165 | 165 | </div> |
| 166 | 166 | </li> |
| 167 | 167 | </ul> |
| @@ -187,8 +187,8 @@ | ||
| 187 | 187 | <div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent"></div> |
| 188 | 188 | </div> |
| 189 | 189 | <div class="p-4"> |
| 190 | <h3 class="font-semibold text-gray-900 text-sm group-hover:text-deep-brown" :text="court.name"></h3> | |
| 191 | <p class="mt-1 text-gray-500 text-xs" :text="`${court.city}, ${court.state}`"></p> | |
| 190 | <h3 class="font-semibold text-gray-900 text-sm group-hover:text-deep-brown">{{ court.name }}</h3> | |
| 191 | <p class="mt-1 text-gray-500 text-xs">{{ `${court.city}, ${court.state}` }}</p> | |
| 192 | 192 | </div> |
| 193 | 193 | </StxLink> |
| 194 | 194 | </div> |
| @@ -320,7 +320,7 @@ | ||
| 320 | 320 | x-class="photoUploading() ? 'opacity-60 pointer-events-none' : ''" |
| 321 | 321 | > |
| 322 | 322 | <svg :if="photoUploading()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 323 | <span :text="photoUploading() ? 'Uploading…' : 'Change photo'"></span> | |
| 323 | <span>{{ photoUploading() ? 'Uploading…' : 'Change photo' }}</span> | |
| 324 | 324 | <input |
| 325 | 325 | type="file" |
| 326 | 326 | accept="image/jpeg,image/png,image/webp" |
| @@ -333,9 +333,7 @@ | ||
| 333 | 333 | <div |
| 334 | 334 | role="alert" |
| 335 | 335 | class="mt-2 text-red-600 text-xs" |
| 336 | x-class="photoError() ? '' : 'hidden'" | |
| 337 | :text="photoError()" | |
| 338 | ></div> | |
| 336 | x-class="photoError() ? '' : 'hidden'">{{ photoError() }}</div> | |
| 339 | 337 | </div> |
| 340 | 338 | |
| 341 | 339 | <div class="grid gap-x-6 gap-y-5 grid-cols-1 sm:grid-cols-2"> |
| @@ -369,7 +367,7 @@ | ||
| 369 | 367 | <div class="flex items-center justify-between gap-x-4 pt-2"> |
| 370 | 368 | <div> |
| 371 | 369 | <p :if="saveStatus === 'saved'" class="text-emerald-700 text-sm">Saved.</p> |
| 372 | <p :if="saveStatus === 'error'" class="text-red-600 text-sm" :text="saveError"></p> | |
| 370 | <p :if="saveStatus === 'error'" class="text-red-600 text-sm">{{ saveError }}</p> | |
| 373 | 371 | </div> |
| 374 | 372 | <button |
| 375 | 373 | type="button" |
| @@ -378,7 +376,7 @@ | ||
| 378 | 376 | class="inline-flex items-center justify-center px-4 py-2 font-semibold text-sm text-white bg-gray-900 hover:bg-gray-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-md transition-colors shadow-sm" |
| 379 | 377 | > |
| 380 | 378 | <svg :if="saveStatus() === 'saving'" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 381 | <span :text="saveStatus === 'saving' ? 'Saving…' : 'Save changes'"></span> | |
| 379 | <span>{{ saveStatus === 'saving' ? 'Saving…' : 'Save changes' }}</span> | |
| 382 | 380 | </button> |
| 383 | 381 | </div> |
| 384 | 382 | </div> |
| @@ -435,7 +433,7 @@ | ||
| 435 | 433 | <div class="flex items-center justify-between gap-x-4 pt-2"> |
| 436 | 434 | <div> |
| 437 | 435 | <p :if="pwStatus === 'saved'" class="text-emerald-700 text-sm">Password updated.</p> |
| 438 | <p :if="pwStatus === 'error'" class="text-red-600 text-sm" :text="pwError"></p> | |
| 436 | <p :if="pwStatus === 'error'" class="text-red-600 text-sm">{{ pwError }}</p> | |
| 439 | 437 | </div> |
| 440 | 438 | <button |
| 441 | 439 | type="button" |
| @@ -444,7 +442,7 @@ | ||
| 444 | 442 | class="inline-flex items-center justify-center px-4 py-2 font-semibold text-sm text-white bg-gray-900 hover:bg-gray-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-md transition-colors shadow-sm" |
| 445 | 443 | > |
| 446 | 444 | <svg :if="pwStatus() === 'saving'" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 447 | <span :text="pwStatus === 'saving' ? 'Updating…' : 'Update password'"></span> | |
| 445 | <span>{{ pwStatus === 'saving' ? 'Updating…' : 'Update password' }}</span> | |
| 448 | 446 | </button> |
| 449 | 447 | </div> |
| 450 | 448 | </div> |
| @@ -499,9 +497,7 @@ | ||
| 499 | 497 | <div |
| 500 | 498 | role="alert" |
| 501 | 499 | class="px-3 py-2 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 502 | x-class="credentialStatus === 'error' ? '' : 'hidden'" | |
| 503 | :text="credentialError()" | |
| 504 | ></div> | |
| 500 | x-class="credentialStatus === 'error' ? '' : 'hidden'">{{ credentialError() }}</div> | |
| 505 | 501 | |
| 506 | 502 | <div |
| 507 | 503 | role="status" |
| @@ -517,7 +513,7 @@ | ||
| 517 | 513 | class="inline-flex items-center justify-center px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50" |
| 518 | 514 | > |
| 519 | 515 | <svg :if="credentialStatus() === 'saving'" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 520 | <span :text="credentialStatus === 'saving' ? 'Submitting…' : 'Submit claim for verification'"></span> | |
| 516 | <span>{{ credentialStatus === 'saving' ? 'Submitting…' : 'Submit claim for verification' }}</span> | |
| 521 | 517 | </button> |
| 522 | 518 | </div> |
| 523 | 519 | </div> |
| @@ -550,9 +546,7 @@ | ||
| 550 | 546 | type="button" |
| 551 | 547 | @click="exportData()" |
| 552 | 548 | x-disabled="exporting" |
| 553 | class="px-3 py-1.5 font-medium text-gray-700 text-sm bg-white hover:bg-gray-50 border border-gray-200 hover:border-gray-300 rounded-md transition-colors disabled:opacity-50" | |
| 554 | :text="exporting() ? 'Preparing…' : 'Export data'" | |
| 555 | ></button> | |
| 549 | class="px-3 py-1.5 font-medium text-gray-700 text-sm bg-white hover:bg-gray-50 border border-gray-200 hover:border-gray-300 rounded-md transition-colors disabled:opacity-50">{{ exporting() ? 'Preparing…' : 'Export data' }}</button> | |
| 556 | 550 | </div> |
| 557 | 551 | |
| 558 | 552 | <!-- Delete account --> |
| @@ -580,7 +574,7 @@ | ||
| 580 | 574 | autocomplete="current-password" |
| 581 | 575 | class="block mt-2 px-3 py-2 w-full max-w-sm text-gray-900 text-sm bg-white border border-gray-300 rounded-md focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-200" |
| 582 | 576 | > |
| 583 | <p role="alert" class="mt-2 text-red-700 text-xs" x-class="deleteError() ? '' : 'hidden'" :text="deleteError()"></p> | |
| 577 | <p role="alert" class="mt-2 text-red-700 text-xs" x-class="deleteError() ? '' : 'hidden'">{{ deleteError() }}</p> | |
| 584 | 578 | <div class="flex gap-x-3 mt-3"> |
| 585 | 579 | <button |
| 586 | 580 | type="button" |
| @@ -589,7 +583,7 @@ | ||
| 589 | 583 | class="inline-flex items-center justify-center px-4 py-2 font-semibold text-white text-sm bg-red-600 hover:bg-red-700 rounded-md disabled:opacity-50" |
| 590 | 584 | > |
| 591 | 585 | <svg :if="deleting()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 592 | <span :text="deleting() ? 'Deleting…' : 'Permanently delete'"></span> | |
| 586 | <span>{{ deleting() ? 'Deleting…' : 'Permanently delete' }}</span> | |
| 593 | 587 | </button> |
| 594 | 588 | <button |
| 595 | 589 | type="button" |
| @@ -17,8 +17,8 @@ | ||
| 17 | 17 | x-class="t.type === 'error' ? 'bg-red-50 border-red-200 text-red-800' : t.type === 'success' ? 'bg-green-50 border-green-200 text-green-800' : t.type === 'warning' ? 'bg-amber-50 border-amber-200 text-amber-900' : 'bg-white border-gray-200 text-gray-800'" |
| 18 | 18 | > |
| 19 | 19 | <div class="flex-1 min-w-0"> |
| 20 | <p :if="t.title" class="font-semibold text-sm" :text="t.title"></p> | |
| 21 | <p class="text-sm break-words" :text="t.message"></p> | |
| 20 | <p :if="t.title" class="font-semibold text-sm">{{ t.title }}</p> | |
| 21 | <p class="text-sm break-words">{{ t.message }}</p> | |
| 22 | 22 | </div> |
| 23 | 23 | <span class="flex-shrink-0 text-lg leading-none opacity-40" aria-hidden="true">×</span> |
| 24 | 24 | </div> |
| @@ -189,10 +189,10 @@ | ||
| 189 | 189 | <div class="p-10"> |
| 190 | 190 | <div class="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between"> |
| 191 | 191 | <div class="flex gap-4 items-center"> |
| 192 | <span class="inline-flex items-center justify-center h-20 w-20 font-semibold text-amber-700 text-xl bg-amber-50 ring-2 ring-white/20 rounded-full" :text="initials"></span> | |
| 192 | <span class="inline-flex items-center justify-center h-20 w-20 font-semibold text-amber-700 text-xl bg-amber-50 ring-2 ring-white/20 rounded-full">{{ initials }}</span> | |
| 193 | 193 | <div> |
| 194 | 194 | <div class="flex flex-wrap items-center gap-2"> |
| 195 | <h1 class="font-bold text-2xl text-white" :text="displayName"></h1> | |
| 195 | <h1 class="font-bold text-2xl text-white">{{ displayName }}</h1> | |
| 196 | 196 | <!-- Verified-credential badge (bench-review#37). Emerald |
| 197 | 197 | check pill, only rendered when the API surfaced a |
| 198 | 198 | non-null credential block (admin-approved). --> |
| @@ -207,9 +207,9 @@ | ||
| 207 | 207 | Verified |
| 208 | 208 | </span> |
| 209 | 209 | </div> |
| 210 | <p :if="memberSince" class="mt-1 text-gray-400 text-sm" :text="`Member since ${memberSince}`"></p> | |
| 211 | <p :if="verifiedCredential" class="text-gray-400 text-xs" :text="credentialLabel"></p> | |
| 212 | <p class="mt-1 text-gray-400 text-xs" :text="reviewCountLabel"></p> | |
| 210 | <p :if="memberSince" class="mt-1 text-gray-400 text-sm">{{ `Member since ${memberSince}` }}</p> | |
| 211 | <p :if="verifiedCredential" class="text-gray-400 text-xs">{{ credentialLabel }}</p> | |
| 212 | <p class="mt-1 text-gray-400 text-xs">{{ reviewCountLabel }}</p> | |
| 213 | 213 | </div> |
| 214 | 214 | </div> |
| 215 | 215 | <div class="flex flex-wrap gap-2"> |
| @@ -232,25 +232,25 @@ | ||
| 232 | 232 | <div class="grid gap-4 grid-cols-2 mt-8 lg:grid-cols-4"> |
| 233 | 233 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 234 | 234 | <p class="text-gray-500 text-sm">Reviews</p> |
| 235 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="user()?.review_count ?? 0"></p> | |
| 235 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ user()?.review_count ?? 0 }}</p> | |
| 236 | 236 | </div> |
| 237 | 237 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 238 | 238 | <p class="text-gray-500 text-sm">Judges Reviewed</p> |
| 239 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="user()?.judges_reviewed ?? 0"></p> | |
| 239 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ user()?.judges_reviewed ?? 0 }}</p> | |
| 240 | 240 | </div> |
| 241 | 241 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 242 | 242 | <p class="text-gray-500 text-sm">Average Rating</p> |
| 243 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="user()?.avg_rating == null ? '—' : user().avg_rating"></p> | |
| 243 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ user()?.avg_rating == null ? '—' : user().avg_rating }}</p> | |
| 244 | 244 | </div> |
| 245 | 245 | <div class="p-6 bg-white border border-gray-200 rounded-xl"> |
| 246 | 246 | <p class="text-gray-500 text-sm">Total Likes</p> |
| 247 | <p class="mt-1 font-semibold text-3xl text-gray-900" :text="user()?.total_likes ?? 0"></p> | |
| 247 | <p class="mt-1 font-semibold text-3xl text-gray-900">{{ user()?.total_likes ?? 0 }}</p> | |
| 248 | 248 | </div> |
| 249 | 249 | </div> |
| 250 | 250 | |
| 251 | 251 | <!-- Reviews list --> |
| 252 | 252 | <div class="mt-10"> |
| 253 | <h2 class="font-semibold text-base text-gray-900" :text="`${displayName}'s published reviews`"></h2> | |
| 253 | <h2 class="font-semibold text-base text-gray-900">{{ `${displayName}'s published reviews` }}</h2> | |
| 254 | 254 | |
| 255 | 255 | <!-- Loading skeleton --> |
| 256 | 256 | <ul :if="!reviewsLoaded" class="mt-6 space-y-4"> |
| @@ -269,7 +269,7 @@ | ||
| 269 | 269 | <!-- Empty state --> |
| 270 | 270 | <div :if="reviewsLoaded && reviews.length === 0" class="mt-6 p-10 text-center bg-white border border-gray-200 rounded-lg"> |
| 271 | 271 | <p class="font-semibold text-gray-900 text-base">No published reviews yet.</p> |
| 272 | <p class="mt-2 text-gray-500 text-sm" :text="isOwnProfile() ? 'When a moderator approves your first review, it lands here.' : 'This reviewer hasn\\'t had a review published yet.'"></p> | |
| 272 | <p class="mt-2 text-gray-500 text-sm">{{ isOwnProfile() ? 'When a moderator approves your first review, it lands here.' : 'This reviewer hasn\\'t had a review published yet.' }}</p> | |
| 273 | 273 | </div> |
| 274 | 274 | |
| 275 | 275 | <!-- Reviews --> |
| @@ -278,22 +278,20 @@ | ||
| 278 | 278 | <div class="flex flex-wrap items-baseline gap-x-3 gap-y-1"> |
| 279 | 279 | <StxLink |
| 280 | 280 | x-to="`/article/${r.id}`" |
| 281 | class="font-semibold text-gray-900 text-base hover:underline underline-offset-2 decoration-gray-300" | |
| 282 | :text="r.title" | |
| 283 | ></StxLink> | |
| 281 | class="font-semibold text-gray-900 text-base hover:underline underline-offset-2 decoration-gray-300">{{ r.title }}</StxLink> | |
| 284 | 282 | <span :if="r.judge" class="text-gray-500 text-xs"> |
| 285 | 283 | on |
| 286 | <StxLink x-to="`/judges/${r.judge.id}/profile`" class="text-gray-700 hover:underline underline-offset-2" :text="r.judge.name"></StxLink> | |
| 284 | <StxLink x-to="`/judges/${r.judge.id}/profile`" class="text-gray-700 hover:underline underline-offset-2">{{ r.judge.name }}</StxLink> | |
| 287 | 285 | </span> |
| 288 | 286 | </div> |
| 289 | 287 | <div class="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 text-gray-500 text-xs"> |
| 290 | <span :text="formatDate(r.created_at)"></span> | |
| 288 | <span>{{ formatDate(r.created_at) }}</span> | |
| 291 | 289 | <span class="text-gray-300">·</span> |
| 292 | <span class="inline-flex items-center gap-1 tabular-nums" :text="`${r.rating}/5`"></span> | |
| 290 | <span class="inline-flex items-center gap-1 tabular-nums">{{ `${r.rating}/5` }}</span> | |
| 293 | 291 | <span :if="r.likes && r.likes > 0" class="text-gray-300">·</span> |
| 294 | <span :if="r.likes && r.likes > 0" class="inline-flex items-center gap-1" :text="`${r.likes} ${r.likes === 1 ? 'reaction' : 'reactions'}`"></span> | |
| 292 | <span :if="r.likes && r.likes > 0" class="inline-flex items-center gap-1">{{ `${r.likes} ${r.likes === 1 ? 'reaction' : 'reactions'}` }}</span> | |
| 295 | 293 | </div> |
| 296 | <p class="mt-3 text-gray-600 text-sm line-clamp-2" :text="snippet(r.content)"></p> | |
| 294 | <p class="mt-3 text-gray-600 text-sm line-clamp-2">{{ snippet(r.content) }}</p> | |
| 297 | 295 | <div class="flex flex-wrap items-center gap-x-4 mt-4 pt-3 border-t border-gray-100 text-sm"> |
| 298 | 296 | <StxLink x-to="`/article/${r.id}`" class="font-medium text-gray-900 hover:underline underline-offset-2"> |
| 299 | 297 | Read review → |
| @@ -97,15 +97,13 @@ | ||
| 97 | 97 | <!-- Error (bad/expired link) --> |
| 98 | 98 | <div x-class="phase() === 'error' ? '' : 'hidden'"> |
| 99 | 99 | <h1 class="font-serif font-bold text-2xl text-gray-900">Verification failed</h1> |
| 100 | <p class="mt-2 text-gray-600 text-sm" :text="message()"></p> | |
| 100 | <p class="mt-2 text-gray-600 text-sm">{{ message() }}</p> | |
| 101 | 101 | <button |
| 102 | 102 | type="button" |
| 103 | 103 | @click="resend()" |
| 104 | 104 | x-disabled="resending" |
| 105 | class="mt-5 inline-flex items-center px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50" | |
| 106 | :text="resending() ? 'Sending…' : 'Resend verification email'" | |
| 107 | ></button> | |
| 108 | <p class="mt-3 text-gray-500 text-xs" :text="resendNote()"></p> | |
| 105 | class="mt-5 inline-flex items-center px-4 py-2 font-semibold text-white text-sm bg-gray-900 hover:bg-gray-800 rounded-md disabled:opacity-50">{{ resending() ? 'Sending…' : 'Resend verification email' }}</button> | |
| 106 | <p class="mt-3 text-gray-500 text-xs">{{ resendNote() }}</p> | |
| 109 | 107 | </div> |
| 110 | 108 | |
| 111 | 109 | <!-- Landing (arrived at /verify-email with no token) --> |
| @@ -116,10 +114,8 @@ | ||
| 116 | 114 | type="button" |
| 117 | 115 | @click="resend()" |
| 118 | 116 | x-disabled="resending" |
| 119 | class="mt-5 inline-flex items-center px-4 py-2 font-semibold text-gray-800 text-sm bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50" | |
| 120 | :text="resending() ? 'Sending…' : 'Resend verification email'" | |
| 121 | ></button> | |
| 122 | <p class="mt-3 text-gray-500 text-xs" :text="resendNote()"></p> | |
| 117 | class="mt-5 inline-flex items-center px-4 py-2 font-semibold text-gray-800 text-sm bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50">{{ resending() ? 'Sending…' : 'Resend verification email' }}</button> | |
| 118 | <p class="mt-3 text-gray-500 text-xs">{{ resendNote() }}</p> | |
| 123 | 119 | </div> |
| 124 | 120 | </div> |
| 125 | 121 | </div> |
| @@ -86,16 +86,13 @@ | ||
| 86 | 86 | role="alert" |
| 87 | 87 | aria-live="polite" |
| 88 | 88 | class="px-4 py-3 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 89 | x-class="submitError() ? '' : 'hidden'" | |
| 90 | :text="submitError()" | |
| 91 | ></div> | |
| 89 | x-class="submitError() ? '' : 'hidden'">{{ submitError() }}</div> | |
| 92 | 90 | |
| 93 | 91 | <button |
| 94 | 92 | type="submit" |
| 95 | 93 | x-disabled="loading" |
| 96 | 94 | class="flex justify-center px-3 py-1.5 w-full font-semibold text-sm/6 text-white bg-gray-900 hover:bg-gray-800 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-gray-900 focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" |
| 97 | :text="loading() ? 'Signing in...' : 'Sign in to admin'" | |
| 98 | >Sign in to admin</button> | |
| 95 | >{{ loading() ? 'Signing in...' : 'Sign in to admin' }}</button> | |
| 99 | 96 | </form> |
| 100 | 97 | </div> |
| 101 | 98 | |
| @@ -32,16 +32,16 @@ | ||
| 32 | 32 | </div> |
| 33 | 33 | <div class="flex-1"> |
| 34 | 34 | <div class="flex gap-x-4 items-center text-xs"> |
| 35 | <time :datetime="post.dateTime" class="text-gray-500" :text="post.date"></time> | |
| 35 | <time :datetime="post.dateTime" class="text-gray-500">{{ post.date }}</time> | |
| 36 | 36 | </div> |
| 37 | 37 | <div class="relative max-w-xl group"> |
| 38 | <h3 class="mt-3 font-semibold leading-6 text-gray-900 text-lg group-hover:text-softBrown duration-200 transition-colors" :text="post.title"></h3> | |
| 38 | <h3 class="mt-3 font-semibold leading-6 text-gray-900 text-lg group-hover:text-softBrown duration-200 transition-colors">{{ post.title }}</h3> | |
| 39 | 39 | <div class="flex gap-x-4 items-center mt-5"> |
| 40 | 40 | <div class="leading-6 text-sm"> |
| 41 | <p class="font-semibold text-gray-900" :text="post.author.name"></p> | |
| 41 | <p class="font-semibold text-gray-900">{{ post.author.name }}</p> | |
| 42 | 42 | </div> |
| 43 | 43 | </div> |
| 44 | <p class="mt-5 leading-6 line-clamp-3 text-gray-600 text-sm" :text="post.content"></p> | |
| 44 | <p class="mt-5 leading-6 line-clamp-3 text-gray-600 text-sm">{{ post.content }}</p> | |
| 45 | 45 | </div> |
| 46 | 46 | </div> |
| 47 | 47 | </article> |
| @@ -148,7 +148,7 @@ | ||
| 148 | 148 | </div> |
| 149 | 149 | </div> |
| 150 | 150 | |
| 151 | <p x-class="submitError() ? 'text-red-600 text-sm' : 'hidden'" :text="submitError()"></p> | |
| 151 | <p x-class="submitError() ? 'text-red-600 text-sm' : 'hidden'">{{ submitError() }}</p> | |
| 152 | 152 | |
| 153 | 153 | <button |
| 154 | 154 | type="submit" |
| @@ -31,8 +31,8 @@ | ||
| 31 | 31 | <div class="flex gap-x-6 items-center"> |
| 32 | 32 | <img x-src="court.image" x-alt="court.name" class="object-cover flex-none h-16 w-16 ring-1 ring-gray-900/10 rounded-lg"> |
| 33 | 33 | <h1> |
| 34 | <div class="text-gray-500 text-sm/6">Court House <span class="text-gray-700" :text="`#${court.id}`"></span></div> | |
| 35 | <div class="mt-1 font-semibold text-base text-gray-900" :text="court.name"></div> | |
| 34 | <div class="text-gray-500 text-sm/6">Court House <span class="text-gray-700">{{ `#${court.id}` }}</span></div> | |
| 35 | <div class="mt-1 font-semibold text-base text-gray-900">{{ court.name }}</div> | |
| 36 | 36 | </h1> |
| 37 | 37 | </div> |
| 38 | 38 | <div class="flex gap-x-4 items-center sm:gap-x-6"> |
| @@ -91,15 +91,15 @@ | ||
| 91 | 91 | <div class="p-4"> |
| 92 | 92 | <dl class="flex flex-wrap"> |
| 93 | 93 | <div class="flex-auto"> |
| 94 | <dt class="font-semibold text-gray-900 text-sm/6" :text="court.name"></dt> | |
| 95 | <dd class="mt-1 text-gray-700 text-sm" :text="`${court.city}, ${court.state}`"></dd> | |
| 94 | <dt class="font-semibold text-gray-900 text-sm/6">{{ court.name }}</dt> | |
| 95 | <dd class="mt-1 text-gray-700 text-sm">{{ `${court.city}, ${court.state}` }}</dd> | |
| 96 | 96 | </div> |
| 97 | 97 | <div class="flex flex-none gap-x-4 mt-6 pt-6 px-2 w-full border-gray-900/5 border-t"> |
| 98 | 98 | <dt class="flex-none"> |
| 99 | 99 | <span class="sr-only">Address</span> |
| 100 | 100 | <Icon name="map-pin" size="20" class="text-gray-400" /> |
| 101 | 101 | </dt> |
| 102 | <dd class="text-gray-700 text-sm/6" :text="`${court.address} ${court.city}, ${court.state} ${court.zipCode}`"></dd> | |
| 102 | <dd class="text-gray-700 text-sm/6">{{ `${court.address} ${court.city}, ${court.state} ${court.zipCode}` }}</dd> | |
| 103 | 103 | </div> |
| 104 | 104 | <div class="flex flex-none gap-x-4 mt-4 px-2 w-full"> |
| 105 | 105 | <dt class="flex-none"> |
| @@ -67,14 +67,14 @@ | ||
| 67 | 67 | class="flex items-center justify-between w-full text-gray-900 text-left" |
| 68 | 68 | @click="open.set(open() === idx ? -1 : idx)" |
| 69 | 69 | > |
| 70 | <span class="font-semibold text-base" :text="faq.q"></span> | |
| 70 | <span class="font-semibold text-base">{{ faq.q }}</span> | |
| 71 | 71 | <span class="flex items-center ml-6 h-7"> |
| 72 | 72 | <Icon name="chevron-down" size="24" class="duration-200 transition-transform" x-class="open() === idx ? 'rotate-180' : ''" /> |
| 73 | 73 | </span> |
| 74 | 74 | </button> |
| 75 | 75 | </dt> |
| 76 | 76 | <dd :if="open() === idx" class="mt-4 pr-12"> |
| 77 | <p class="leading-7 text-base text-gray-600" :text="faq.a"></p> | |
| 77 | <p class="leading-7 text-base text-gray-600">{{ faq.a }}</p> | |
| 78 | 78 | </dd> |
| 79 | 79 | </div> |
| 80 | 80 | </dl> |
| @@ -74,7 +74,7 @@ | ||
| 74 | 74 | </div> |
| 75 | 75 | </div> |
| 76 | 76 | |
| 77 | <p :if="submitError()" class="text-red-600 text-sm" :text="submitError()"></p> | |
| 77 | <p :if="submitError()" class="text-red-600 text-sm">{{ submitError() }}</p> | |
| 78 | 78 | |
| 79 | 79 | <button |
| 80 | 80 | type="submit" |
| @@ -94,7 +94,7 @@ | ||
| 94 | 94 | </div> |
| 95 | 95 | <h2 class="mt-4 font-semibold text-gray-900 text-lg">Check your email</h2> |
| 96 | 96 | <p class="mt-2 text-gray-600 text-sm"> |
| 97 | If an account exists for <span class="font-medium" :text="email"></span>, we've sent a password reset link. | |
| 97 | If an account exists for <span class="font-medium">{{ email }}</span>, we've sent a password reset link. | |
| 98 | 98 | </p> |
| 99 | 99 | </div> |
| 100 | 100 | |
| @@ -181,7 +181,7 @@ | ||
| 181 | 181 | </form> |
| 182 | 182 | <div class="flex flex-wrap gap-2 mt-4 text-gray-500 text-xs"> |
| 183 | 183 | <span class="font-medium">Popular:</span> |
| 184 | <StxLink :for="cat in popularCategories" x-to="`/search?type=${cat.name}`" class="hover:text-gray-700 hover:underline" :text="cat.name"></StxLink> | |
| 184 | <StxLink :for="cat in popularCategories" x-to="`/search?type=${cat.name}`" class="hover:text-gray-700 hover:underline">{{ cat.name }}</StxLink> | |
| 185 | 185 | </div> |
| 186 | 186 | </div> |
| 187 | 187 | </div> |
| @@ -241,13 +241,13 @@ | ||
| 241 | 241 | <div class="flex gap-4 items-center"> |
| 242 | 242 | <img class="object-cover h-14 w-14 ring-2 ring-gray-50 rounded-full grayscale group-hover:grayscale-0 duration-200 transition-all" x-src="judge.image_url" x-alt="judge.name"> |
| 243 | 243 | <div class="min-w-0 flex-1"> |
| 244 | <h3 class="font-semibold text-gray-900 truncate" :text="judge.name"></h3> | |
| 245 | <p :if="judge.court_name" class="text-gray-500 text-sm truncate" :text="judge.court_name"></p> | |
| 244 | <h3 class="font-semibold text-gray-900 truncate">{{ judge.name }}</h3> | |
| 245 | <p :if="judge.court_name" class="text-gray-500 text-sm truncate">{{ judge.court_name }}</p> | |
| 246 | 246 | </div> |
| 247 | 247 | </div> |
| 248 | 248 | <div class="flex items-center justify-between mt-4 pt-3 border-t border-gray-100"> |
| 249 | <span class="text-amber-700 text-xs font-medium" :text="`${judge.review_count} ${judge.review_count === 1 ? 'review' : 'reviews'} this week`"></span> | |
| 250 | <span :if="judge.avg_rating" class="font-medium text-gray-700 text-xs tabular-nums" :text="`${judge.avg_rating} ★`"></span> | |
| 249 | <span class="text-amber-700 text-xs font-medium">{{ `${judge.review_count} ${judge.review_count === 1 ? 'review' : 'reviews'} this week` }}</span> | |
| 250 | <span :if="judge.avg_rating" class="font-medium text-gray-700 text-xs tabular-nums">{{ `${judge.avg_rating} ★` }}</span> | |
| 251 | 251 | </div> |
| 252 | 252 | </StxLink> |
| 253 | 253 | </div> |
| @@ -264,8 +264,8 @@ | ||
| 264 | 264 | <div class="mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl"> |
| 265 | 265 | <div class="flex items-end justify-between"> |
| 266 | 266 | <div> |
| 267 | <h2 class="font-bold text-3xl text-gray-900 tracking-tight" :text="showTopRated() ? 'Top-rated judges' : 'Featured judges'"></h2> | |
| 268 | <p class="mt-2 text-gray-600" :text="showTopRated() ? 'Highest-rated judges based on published reviews.' : 'A sampling from the directory to get you started.'"></p> | |
| 267 | <h2 class="font-bold text-3xl text-gray-900 tracking-tight">{{ showTopRated() ? 'Top-rated judges' : 'Featured judges' }}</h2> | |
| 268 | <p class="mt-2 text-gray-600">{{ showTopRated() ? 'Highest-rated judges based on published reviews.' : 'A sampling from the directory to get you started.' }}</p> | |
| 269 | 269 | </div> |
| 270 | 270 | <StxLink to="/judges" class="hidden sm:block font-semibold text-gray-700 text-sm hover:text-gray-900"> |
| 271 | 271 | View all judges <span aria-hidden="true">→</span> |
| @@ -283,12 +283,12 @@ | ||
| 283 | 283 | <div class="flex gap-4 items-center"> |
| 284 | 284 | <img class="object-cover h-16 w-16 ring-2 ring-gray-50 rounded-full grayscale group-hover:grayscale-0 duration-200 transition-all" x-src="judge.image_url" x-alt="judge.name"> |
| 285 | 285 | <div class="min-w-0"> |
| 286 | <h3 class="font-semibold text-gray-900 truncate group-hover:text-deep-brown duration-200 transition-colors" :text="judge.name"></h3> | |
| 287 | <p :if="judge.court_name" class="text-gray-500 text-sm truncate" :text="judge.court_name"></p> | |
| 286 | <h3 class="font-semibold text-gray-900 truncate group-hover:text-deep-brown duration-200 transition-colors">{{ judge.name }}</h3> | |
| 287 | <p :if="judge.court_name" class="text-gray-500 text-sm truncate">{{ judge.court_name }}</p> | |
| 288 | 288 | </div> |
| 289 | 289 | </div> |
| 290 | 290 | <div class="flex items-center justify-between mt-4 pt-4 border-gray-100 border-t"> |
| 291 | <span class="font-medium text-amber-700 text-xs tabular-nums" :text="`${judge.avg_rating} ★ · ${judge.review_count} ${judge.review_count === 1 ? 'review' : 'reviews'}`"></span> | |
| 291 | <span class="font-medium text-amber-700 text-xs tabular-nums">{{ `${judge.avg_rating} ★ · ${judge.review_count} ${judge.review_count === 1 ? 'review' : 'reviews'}` }}</span> | |
| 292 | 292 | <span class="font-medium text-gray-700 text-xs hover:text-gray-900">View profile →</span> |
| 293 | 293 | </div> |
| 294 | 294 | </StxLink> |
| @@ -306,9 +306,9 @@ | ||
| 306 | 306 | <div class="flex gap-4 items-center"> |
| 307 | 307 | <img class="object-cover h-16 w-16 ring-2 ring-gray-50 rounded-full grayscale group-hover:grayscale-0 duration-200 transition-all" x-src="judge.photo" x-alt="judge.name"> |
| 308 | 308 | <div class="min-w-0"> |
| 309 | <h3 class="font-semibold text-gray-900 truncate group-hover:text-deep-brown duration-200 transition-colors" :text="judge.name"></h3> | |
| 310 | <p class="text-gray-500 text-sm truncate" :text="judge.court.name"></p> | |
| 311 | <p class="text-gray-400 text-xs" :text="judge.location"></p> | |
| 309 | <h3 class="font-semibold text-gray-900 truncate group-hover:text-deep-brown duration-200 transition-colors">{{ judge.name }}</h3> | |
| 310 | <p class="text-gray-500 text-sm truncate">{{ judge.court.name }}</p> | |
| 311 | <p class="text-gray-400 text-xs">{{ judge.location }}</p> | |
| 312 | 312 | </div> |
| 313 | 313 | </div> |
| 314 | 314 | <div class="flex items-center justify-between mt-4 pt-4 border-gray-100 border-t"> |
| @@ -398,18 +398,18 @@ | ||
| 398 | 398 | > |
| 399 | 399 | <div class="flex items-center justify-between"> |
| 400 | 400 | <div class="flex items-center"> |
| 401 | <div class="flex items-center justify-center h-9 w-9 font-semibold text-gray-500 text-sm bg-gray-100 rounded-full" :text="(review.title || 'A').slice(0, 1).toUpperCase()"></div> | |
| 401 | <div class="flex items-center justify-center h-9 w-9 font-semibold text-gray-500 text-sm bg-gray-100 rounded-full">{{ (review.title || 'A').slice(0, 1).toUpperCase() }}</div> | |
| 402 | 402 | <div class="ml-3"> |
| 403 | 403 | <p class="font-medium text-gray-900 text-sm">Reviewer</p> |
| 404 | <p class="text-gray-500 text-xs" :text="formatDate(review.created_at)"></p> | |
| 404 | <p class="text-gray-500 text-xs">{{ formatDate(review.created_at) }}</p> | |
| 405 | 405 | </div> |
| 406 | 406 | </div> |
| 407 | 407 | <div class="flex items-center"> |
| 408 | 408 | <Icon :for="star in stars" name="star" size="16" x-class="star <= review.rating ? 'text-yellow-400' : 'text-gray-200'" /> |
| 409 | 409 | </div> |
| 410 | 410 | </div> |
| 411 | <h3 class="mt-4 font-semibold line-clamp-2 text-gray-900" :text="review.title"></h3> | |
| 412 | <p class="mt-2 line-clamp-3 text-gray-600 text-sm" :text="review.content"></p> | |
| 411 | <h3 class="mt-4 font-semibold line-clamp-2 text-gray-900">{{ review.title }}</h3> | |
| 412 | <p class="mt-2 line-clamp-3 text-gray-600 text-sm">{{ review.content }}</p> | |
| 413 | 413 | </StxLink> |
| 414 | 414 | </div> |
| 415 | 415 | </div> |
| @@ -433,10 +433,10 @@ | ||
| 433 | 433 | <ul class="grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-5"> |
| 434 | 434 | <li :for="reviewer in activeReviewers" class="p-4 bg-white border border-gray-100 rounded-xl hover:border-emerald-200 hover:shadow-sm transition-all"> |
| 435 | 435 | <StxLink x-to="`/user/${reviewer.id}`" class="flex items-center gap-3"> |
| 436 | <div class="flex items-center justify-center flex-none h-10 w-10 font-semibold text-emerald-700 text-xs bg-emerald-50 ring-1 ring-emerald-200/60 rounded-full" :text="(reviewer.name || '?').charAt(0).toUpperCase()"></div> | |
| 436 | <div class="flex items-center justify-center flex-none h-10 w-10 font-semibold text-emerald-700 text-xs bg-emerald-50 ring-1 ring-emerald-200/60 rounded-full">{{ (reviewer.name || '?').charAt(0).toUpperCase() }}</div> | |
| 437 | 437 | <div class="min-w-0 flex-1"> |
| 438 | <p class="font-medium text-gray-900 text-sm truncate" :text="reviewer.name"></p> | |
| 439 | <p class="text-emerald-700 text-xs" :text="`${reviewer.review_count} ${reviewer.review_count === 1 ? 'review' : 'reviews'}`"></p> | |
| 438 | <p class="font-medium text-gray-900 text-sm truncate">{{ reviewer.name }}</p> | |
| 439 | <p class="text-emerald-700 text-xs">{{ `${reviewer.review_count} ${reviewer.review_count === 1 ? 'review' : 'reviews'}` }}</p> | |
| 440 | 440 | </div> |
| 441 | 441 | </StxLink> |
| 442 | 442 | </li> |
| @@ -168,7 +168,7 @@ | ||
| 168 | 168 | </div> |
| 169 | 169 | </div> |
| 170 | 170 | |
| 171 | <p :show="submitError" class="text-red-600 text-sm" x-text="submitError"></p> | |
| 171 | <p :show="submitError" class="text-red-600 text-sm">{{ submitError }}</p> | |
| 172 | 172 | |
| 173 | 173 | <div class="flex gap-x-4 items-center justify-end"> |
| 174 | 174 | <StxLink to="/judges" class="font-semibold text-gray-700 text-sm">Cancel</StxLink> |
| @@ -108,9 +108,7 @@ | ||
| 108 | 108 | role="alert" |
| 109 | 109 | aria-live="polite" |
| 110 | 110 | class="px-4 py-3 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 111 | x-class="submitError() ? '' : 'hidden'" | |
| 112 | :text="submitError()" | |
| 113 | ></div> | |
| 111 | x-class="submitError() ? '' : 'hidden'">{{ submitError() }}</div> | |
| 114 | 112 | |
| 115 | 113 | <button |
| 116 | 114 | type="submit" |
| @@ -118,7 +116,7 @@ | ||
| 118 | 116 | class="inline-flex items-center justify-center px-3 py-1.5 w-full font-semibold text-sm/6 text-white bg-gray-600 hover:bg-gray-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-gray-600 focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" |
| 119 | 117 | > |
| 120 | 118 | <svg :if="loading()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 121 | <span :text="loading() ? 'Signing in...' : 'Sign in'"></span> | |
| 119 | <span>{{ loading() ? 'Signing in...' : 'Sign in' }}</span> | |
| 122 | 120 | </button> |
| 123 | 121 | </form> |
| 124 | 122 | </div> |
| @@ -142,9 +142,7 @@ | ||
| 142 | 142 | role="alert" |
| 143 | 143 | aria-live="polite" |
| 144 | 144 | class="px-4 py-3 text-red-800 text-sm bg-red-50 border border-red-200 rounded-md" |
| 145 | x-class="submitError() ? '' : 'hidden'" | |
| 146 | :text="submitError()" | |
| 147 | ></div> | |
| 145 | x-class="submitError() ? '' : 'hidden'">{{ submitError() }}</div> | |
| 148 | 146 | |
| 149 | 147 | <button |
| 150 | 148 | type="submit" |
| @@ -152,7 +150,7 @@ | ||
| 152 | 150 | class="inline-flex items-center justify-center px-3 py-1.5 w-full font-semibold text-sm/6 text-white bg-gray-600 hover:bg-gray-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-gray-600 focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" |
| 153 | 151 | > |
| 154 | 152 | <svg :if="loading()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg> |
| 155 | <span :text="loading() ? 'Creating account...' : 'Sign up'"></span> | |
| 153 | <span>{{ loading() ? 'Creating account...' : 'Sign up' }}</span> | |
| 156 | 154 | </button> |
| 157 | 155 | </form> |
| 158 | 156 | </div> |
| @@ -141,15 +141,13 @@ | ||
| 141 | 141 | role="alert" |
| 142 | 142 | aria-live="polite" |
| 143 | 143 | class="text-red-600 text-sm" |
| 144 | x-class="submitError() ? '' : 'hidden'" | |
| 145 | :text="submitError()" | |
| 146 | ></p> | |
| 144 | x-class="submitError() ? '' : 'hidden'">{{ submitError() }}</p> | |
| 147 | 145 | |
| 148 | 146 | <button |
| 149 | 147 | type="submit" |
| 150 | 148 | x-disabled="loading" |
| 151 | 149 | class="inline-flex items-center justify-center px-3 py-1.5 w-full font-semibold text-sm/6 text-white bg-gray-600 hover:bg-gray-500 rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-gray-600 focus-visible:outline-offset-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm" |
| 152 | ><svg :if="loading()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg><span :text="loading() ? 'Saving...' : 'Reset password'"></span></button> | |
| 150 | ><svg :if="loading()" class="animate-spin -ml-0.5 mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"></circle><path d="M4 12a8 8 0 018-8" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path></svg><span>{{ loading() ? 'Saving...' : 'Reset password' }}</span></button> | |
| 153 | 151 | </form> |
| 154 | 152 | </div> |
| 155 | 153 | |