Skip to content

Commit f89a605

Browse files
Merge pull request #142 from CityOfPhiladelphia/fix/311-reports-page-layout
Fix/311 reports page layout
2 parents ec77835 + 611c740 commit f89a605

3 files changed

Lines changed: 95 additions & 77 deletions

File tree

apps/philly-311/frontend/src/components/wizard/StepIndicator.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function handleMouseLeave(ev: MouseEvent) {
8686
padding: 0;
8787
margin: 0;
8888
max-width: 32rem;
89-
grid-template-columns: 14fr 15fr 15fr 15fr 5fr;
89+
grid-template-columns: auto auto auto auto auto;
9090
}
9191
9292
.step-indicator li {

apps/philly-311/frontend/src/pages/ReportPage.vue

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useRouter, useRoute } from 'vue-router'
1111
import StepIndicator from '@/components/wizard/StepIndicator.vue'
1212
import ExitDialog from '@/components/wizard/ExitDialog.vue'
1313
import { PhilaButton } from '@phila/phila-ui-button'
14+
import { Breadcrumbs } from '@phila/phila-ui-breadcrumbs'
1415
import { useReportSubmissionStore } from '@/stores/reportSubmission'
1516
import { useMyCasesStore } from '@/stores/myCases'
1617
import { WIZARD_CAN_ADVANCE_KEY, WIZARD_SHOW_ERRORS_KEY } from '@/composables/useWizardValidity'
@@ -20,21 +21,7 @@ const router = useRouter()
2021
const route = useRoute()
2122
const store = useReportSubmissionStore()
2223
const myCases = useMyCasesStore()
23-
2424
const exitOpen = ref(false)
25-
watch(
26-
exitOpen,
27-
(isOpen, wasOpen) => {
28-
if (isOpen) {
29-
router.push(`?${store.stateToUrlQueryParams()}`)
30-
} else if (!isOpen && wasOpen) {
31-
router.back()
32-
} else {
33-
router.replace({ query: {} })
34-
}
35-
},
36-
{ immediate: true },
37-
)
3825
3926
const STEPS = [
4027
{ title: 'Image', path: '/report' },
@@ -105,46 +92,50 @@ function discardAndExit() {
10592

10693
<template>
10794
<div ref="wizardEl" class="wizard">
108-
<nav class="wizard__crumb" aria-label="Breadcrumb">
109-
<RouterLink to="/">Home</RouterLink> / <span>Report an issue</span>
110-
</nav>
95+
<div class="wizard__header">
96+
<Breadcrumbs :items="[{ label: 'Report an issue' }]" class="wizard__bradcrumbs" />
11197

112-
<StepIndicator
113-
:steps="STEPS"
114-
:current-step="currentStep"
115-
:completed-through="completedThrough"
116-
@navigate="(path: string) => router.push(path)"
117-
/>
98+
<StepIndicator
99+
:steps="STEPS"
100+
:current-step="currentStep"
101+
:completed-through="completedThrough"
102+
class="wizard__steps"
103+
@navigate="(path: string) => router.push(path)"
104+
/>
105+
</div>
118106

119107
<section class="wizard__content">
120108
<RouterView />
121109
</section>
122110

123111
<footer class="wizard__nav">
124-
<button type="button" class="wizard__exit" data-test="wizard-exit" @click="exitOpen = true">
112+
<PhilaButton
113+
size="extra-small"
114+
class="wizard__exit"
115+
data-test="wizard-exit"
116+
@click="exitOpen = true"
117+
>
125118
Exit
126-
</button>
119+
</PhilaButton>
127120

128121
<div class="wizard__nav-right">
129-
<PhilaButton v-if="isImageStep" variant="secondary" data-test="wizard-skip" @click="goNext"
130-
>Skip</PhilaButton
131-
>
132122
<PhilaButton
133-
v-else
123+
v-if="!isImageStep"
134124
variant="secondary"
135125
data-test="wizard-back"
136126
:disabled="!prevPath"
137127
@click="goPrev"
138-
>Back</PhilaButton
139128
>
129+
Back
130+
</PhilaButton>
140131
<PhilaButton
141132
v-if="!isLast"
142133
variant="primary"
143134
data-test="wizard-next"
144135
:disabled="!nextPath"
145136
@click="goNext"
146-
>Next</PhilaButton
147-
>
137+
>{{ isImageStep ? 'Skip' : 'Next' }}
138+
</PhilaButton>
148139
</div>
149140
</footer>
150141

@@ -154,44 +145,73 @@ function discardAndExit() {
154145

155146
<style scoped>
156147
.wizard {
157-
max-width: 980px;
158-
margin: 0 auto;
159-
/* No bottom padding: the sticky nav pins to the scrollport's bottom edge and
160-
content would otherwise show through a padding-sized gap beneath it. */
161-
padding: var(--spacing-m, 1rem) var(--spacing-m, 1rem) 0;
148+
display: grid;
149+
grid-template-areas:
150+
'w_header'
151+
'w_content'
152+
'w_footer';
153+
grid-template-rows: 9rem auto 7rem;
162154
height: 100%;
163-
overflow-y: auto;
155+
width: 100%;
156+
margin: 0 auto;
164157
}
165-
.wizard__crumb {
166-
font-size: 0.875rem;
167-
margin-bottom: var(--spacing-s, 0.75rem);
158+
159+
.wizard__header {
160+
grid-area: w_header;
161+
grid-template-areas:
162+
'w_breadcrumbs'
163+
'w_stepindicator';
164+
grid-template-rows: 3fr 5fr;
165+
width: 100%;
166+
padding: var(--spacing-l, 1.5rem) var(--spacing-xl, 2rem);
168167
}
168+
169+
.wizard__bradcrumbs {
170+
grid-area: w_breadcrumbs;
171+
padding: var(--spacing-xs, 0.5rem);
172+
}
173+
174+
.wizard__bradcrumbs :deep(li) {
175+
margin-bottom: 0;
176+
padding-left: 0;
177+
}
178+
179+
.wizard__steps {
180+
grid-area: w_stepindicator;
181+
display: grid;
182+
place-content: center;
183+
}
184+
169185
.wizard__content {
186+
grid-area: w_content;
170187
padding: var(--spacing-l, 2rem) 0;
171-
min-height: 320px;
188+
margin: 0 var(--scale-1000, 5rem);
189+
overflow-y: auto;
190+
overflow-x: auto;
172191
}
192+
173193
/* Sticky so Exit/Back/Next stay reachable: the wizard scrolls internally
174194
(the shell locks the viewport) and steps like Location put a wheel-capturing
175195
map over most of the content, so controls at the end of the scroll run
176196
can otherwise sit below the fold with no way to reach them. */
177197
.wizard__nav {
178-
position: sticky;
179-
bottom: 0;
180-
background: #fff;
198+
grid-area: w_footer;
181199
display: flex;
182200
justify-content: space-between;
183201
align-items: center;
184202
padding: var(--spacing-m, 1rem) 0;
185203
border-top: 1px solid var(--Schemes-Border-low, #d6d6d6);
204+
background: var(--Schemes-Background, #fff);
186205
}
206+
187207
.wizard__nav-right {
188208
display: flex;
189209
gap: var(--spacing-s, 0.75rem);
210+
margin-right: var(--scale-1000, 5rem);
190211
}
212+
191213
.wizard__exit {
192-
background: none;
193-
border: none;
194-
color: var(--Schemes-Primary, #0f4d90);
195-
cursor: pointer;
214+
margin-left: var(--scale-1000, 5rem);
215+
text-decoration: underline;
196216
}
197217
</style>

apps/philly-311/frontend/src/pages/report/ImageStep.vue

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
which stores the image (mediaUrl) and returns issue-type suggestions for step 2. Optional;
33
Skip/Next both advance. -->
44
<script setup lang="ts">
5-
import { computed, ref } from 'vue'
5+
import { computed, ref, useId } from 'vue'
66
import { processForClassify } from '@/utils/photo'
77
import { useApi } from '@/composables/useApi'
88
import { useReportSubmissionStore } from '@/stores/reportSubmission'
@@ -16,8 +16,10 @@ interface ClassifyResponse {
1616
const store = useReportSubmissionStore()
1717
useWizardValidity(computed(() => true)) // the step is optional
1818
19+
const waitingImageUpload = ref(true)
1920
const classifying = ref(false)
2021
const errorMessage = ref('')
22+
const imageId = useId()
2123
2224
// classifyBody.imgB64 is mutated before each fetchData() call; useApi reads
2325
// opts.body lazily so the latest value is always sent.
@@ -28,25 +30,23 @@ const classify = useApi<ClassifyResponse>({
2830
body: classifyBody,
2931
})
3032
31-
// createObjectURL produces a short-lived preview URL; the browser holds
32-
// the underlying blob alive until revokeObjectURL is called or page unloads.
33-
function makePreview(file: File): string | undefined {
34-
return typeof URL.createObjectURL === 'function' ? URL.createObjectURL(file) : undefined
35-
}
36-
3733
async function onFile(e: Event) {
3834
if (classifying.value) return
39-
const file = (e.target as HTMLInputElement).files?.[0]
35+
const target = e.target as HTMLInputElement
36+
const file = target.files?.[0]
4037
if (!file) return
38+
waitingImageUpload.value = false
4139
classifying.value = true
4240
errorMessage.value = ''
43-
const previewUrl = makePreview(file)
41+
const previewUrl = URL.createObjectURL(file)
42+
const imgElement = document.getElementById(imageId) as HTMLImageElement
43+
imgElement.src = previewUrl
4444
try {
4545
classifyBody.imgB64 = await processForClassify(file)
4646
const result = await classify.fetchData()
4747
if (!result || classify.error.value) {
4848
errorMessage.value = classify.error.value?.message || 'Classification failed.'
49-
if (previewUrl) URL.revokeObjectURL?.(previewUrl)
49+
if (previewUrl) URL.revokeObjectURL(previewUrl)
5050
return
5151
}
5252
store.setPhoto({ mediaUrl: result.imageUrl, previewUrl })
@@ -55,10 +55,10 @@ async function onFile(e: Event) {
5555
)
5656
} catch (err) {
5757
errorMessage.value = (err as Error).message || 'Photo processing failed.'
58-
if (previewUrl) URL.revokeObjectURL?.(previewUrl)
58+
if (previewUrl) URL.revokeObjectURL(previewUrl)
5959
} finally {
6060
classifying.value = false
61-
;(e.target as HTMLInputElement).value = ''
61+
target.value = ''
6262
}
6363
}
6464
</script>
@@ -71,52 +71,47 @@ async function onFile(e: Event) {
7171
type to report. Do not upload any images with personal or sensitive information.
7272
</p>
7373
<p class="image-step__count">{{ store.photo ? '1/1' : '0/1' }}</p>
74-
7574
<div class="image-step__zones">
7675
<label class="image-step__zone">
77-
<span class="image-step__zone-label">Upload</span>
78-
<input type="file" accept="image/*" :disabled="classifying" @change="onFile" />
79-
</label>
80-
<label class="image-step__zone">
81-
<span class="image-step__zone-label">Camera</span>
82-
<input
83-
type="file"
84-
accept="image/*"
85-
capture="environment"
86-
:disabled="classifying"
87-
@change="onFile"
88-
/>
76+
<span v-if="waitingImageUpload" class="image-step__zone-label">Upload</span>
77+
<img :id="imageId" alt="Upload" style="display: none" onload="this.style.display = ''" />
78+
<input type="file" accept="image/*" @change="onFile" />
8979
</label>
9080
</div>
9181

9282
<div role="status">
9383
<p v-if="classifying" class="image-step__status">Analyzing your photo…</p>
94-
<p v-if="store.photo" class="image-step__status">Photo added.</p>
84+
<p v-else-if="store.photo" class="image-step__status">Photo added.</p>
85+
<p v-else-if="errorMessage" role="alert" class="image-step__error">{{ errorMessage }}</p>
9586
</div>
96-
<p v-if="errorMessage" role="alert" class="image-step__error">{{ errorMessage }}</p>
9787
</div>
9888
</template>
9989

10090
<style scoped>
10191
.image-step {
10292
max-width: 640px;
10393
}
94+
10495
.image-step__title {
10596
font-size: 1.25rem;
10697
font-weight: 700;
10798
margin: 0 0 var(--spacing-s, 0.5rem);
10899
}
100+
109101
.image-step__note {
110102
color: var(--Schemes-On-Surface-Variant, #4a4a4a);
111103
margin: 0 0 var(--spacing-s, 0.5rem);
112104
}
105+
113106
.image-step__count {
114107
margin: 0 0 var(--spacing-s, 0.5rem);
115108
}
109+
116110
.image-step__zones {
117111
display: flex;
118112
gap: var(--spacing-m, 1rem);
119113
}
114+
120115
.image-step__zone {
121116
flex: 1;
122117
min-height: 180px;
@@ -127,16 +122,19 @@ async function onFile(e: Event) {
127122
justify-content: center;
128123
cursor: pointer;
129124
}
125+
130126
.image-step__zone input {
131127
position: absolute;
132128
width: 1px;
133129
height: 1px;
134130
opacity: 0;
135131
}
132+
136133
.image-step__zone:focus-within {
137134
outline: 2px solid var(--Schemes-Primary, #0f4d90);
138135
outline-offset: 2px;
139136
}
137+
140138
.image-step__error {
141139
color: var(--Schemes-Error, #c0392b);
142140
}

0 commit comments

Comments
 (0)