@@ -375,7 +295,7 @@ function FaceCard({
priority,
reduceMotion,
}: {
- submission: CleanAirSubmission;
+ submission: CleanAirSubmissionWithId;
priority: boolean;
reduceMotion: boolean | null;
}) {
@@ -388,21 +308,6 @@ function FaceCard({
const rotateX = useSpring(transformedRotateX, SMOOTH_SPRING);
const rotateY = useSpring(transformedRotateY, SMOOTH_SPRING);
- const glareX = useTransform(pointerX, [0, 1], [0, 100]);
- const glareY = useTransform(pointerY, [0, 1], [0, 100]);
-
- const glareBackground = useMotionTemplate`
- radial-gradient(
- circle at ${glareX}% ${glareY}%,
- rgba(255,255,255,0.30) 0%,
- rgba(255,255,255,0.12) 16%,
- rgba(255,255,255,0) 44%
- )
- `;
-
- const pm25 = submission.pm25Value ?? 0;
- const category = getCategoryStyle(submission.aqiCategory, pm25);
-
const displayName =
submission.displayName?.trim() ||
submission.locationName?.trim() ||
@@ -469,7 +374,7 @@ function FaceCard({
rotateY: reduceMotion ? 0 : rotateY,
transformPerspective: 1000,
}}
- className="group relative aspect-[3/4] w-full overflow-hidden rounded-xl border border-white/15 bg-blue-950 shadow-[0_28px_70px_-35px_rgba(2,6,23,0.95)] [transform-style:preserve-3d] will-change-transform"
+ className="group relative aspect-square w-full overflow-hidden rounded-xl border border-white/15 bg-[#005257] shadow-[0_28px_70px_-35px_rgba(2,6,23,0.95)] [transform-style:preserve-3d] will-change-transform"
>
-
-
-
-
-
- {!reduceMotion && (
-
- )}
-
-
-
-
-
-
-
-
- Clean Air Forum
-
-
-
- {EVENT_LOCATION_AND_YEAR}
-
-
-
-
-
-
-
- {displayName}
-
-
-
- {location}
-
-
-
-
-
- {formatPm25(submission.pm25Value)}
-
-
-
-
- PM2.5 µg/m³
-
-
-
- {category.label}
-
-
-
-
-
-
- Shared from the AirQo app
-
-
-
- {EVENT_DATES_BADGE}
-
-
-
);
}
@@ -663,7 +434,7 @@ function EmptyState({ reduceMotion }: { reduceMotion: boolean | null }) {
/>
No faces yet
-
+
Be the first to share an air quality selfie from the Africa Clean Air
Forum.
@@ -747,7 +518,7 @@ function ErrorState({
We could not load the selfie wall
-
+
Please check the connection and try again.
@@ -769,7 +540,7 @@ function ErrorState({
scale: 0.96,
}
}
- className="mt-5 rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-950/20 transition-colors hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-blue-600"
+ className="mt-5 rounded-lg bg-[#39BFC7] px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-[#005257]/20 transition-colors hover:bg-[#39BFC7]/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#39BFC7]"
>
Try again
@@ -836,7 +607,7 @@ function DesktopPagination({
<>
)}
{(reduceMotion || isPaused) && (
-
+
)}
>
)}
@@ -940,7 +711,9 @@ export default function FacesOfCleanAirPage() {
const shouldReduceMotion = useReducedMotion();
const isMobile = useMediaQuery(MOBILE_MEDIA_QUERY);
- const [submissions, setSubmissions] = useState([]);
+ const [submissions, setSubmissions] = useState(
+ [],
+ );
const [fetchState, setFetchState] = useState('idle');
const [page, setPage] = useState(0);
const [direction, setDirection] = useState(1);
@@ -1181,7 +954,7 @@ export default function FacesOfCleanAirPage() {
className="relative h-[100svh] overflow-hidden sm:h-auto sm:min-h-[100svh]"
style={{
background:
- 'linear-gradient(180deg, #02143B 0%, #145FFF 50%, #FFFFFF 100%)',
+ 'linear-gradient(180deg, #005257 0%, #39BFC7 50%, #FFFFFF 100%)',
}}
>
@@ -1411,10 +1184,7 @@ export default function FacesOfCleanAirPage() {
>
{visibleItems.map((submission, index) => (
`users/selfies/${encodeURIComponent(id)}`,
},
PREDICT: {
DAILY_FORECAST: 'predict/daily-forecast',
diff --git a/src/website/src/services/external/faces-of-clean-air.service.ts b/src/website/src/services/external/faces-of-clean-air.service.ts
index 3583ee76b9..c30c4191a3 100644
--- a/src/website/src/services/external/faces-of-clean-air.service.ts
+++ b/src/website/src/services/external/faces-of-clean-air.service.ts
@@ -1,7 +1,8 @@
import { API_ROUTES, BaseApiService, ServiceOptions } from '@/services/api';
export interface CleanAirSubmission {
- id: string;
+ id?: string | null;
+ _id?: string | null;
eventId: string;
imageUrl: string;
locationName: string | null;
@@ -12,6 +13,10 @@ export interface CleanAirSubmission {
createdAt: string;
}
+export type CleanAirSubmissionWithId = CleanAirSubmission & {
+ id: string;
+};
+
class FacesOfCleanAirService extends BaseApiService {
constructor() {
super('FacesOfCleanAirService');
@@ -20,7 +25,7 @@ class FacesOfCleanAirService extends BaseApiService {
async getSubmissions(
eventId: string,
options: ServiceOptions = {},
- ): Promise {
+ ): Promise {
const response = await this.get<{ selfies: CleanAirSubmission[] }>(
API_ROUTES.USERS.SELFIES,
{ eventId },
@@ -28,7 +33,26 @@ class FacesOfCleanAirService extends BaseApiService {
);
if (response.success && response.data) {
- return response.data.selfies ?? [];
+ const selfies = response.data.selfies ?? [];
+
+ return selfies
+ .map((submission) => {
+ const normalizedId =
+ submission.id?.trim() || submission._id?.trim() || '';
+
+ if (!normalizedId) {
+ return null;
+ }
+
+ return {
+ ...submission,
+ id: normalizedId,
+ };
+ })
+ .filter(
+ (submission): submission is CleanAirSubmissionWithId =>
+ submission !== null,
+ );
}
return [];