Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions mobile/src/assets/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -603,11 +603,14 @@
},
"add": "Add hours",
"add_short": "Add",
"sent_toast": "{{n}}h sent for approval",
"at": "At {{name}}",
"totals": {
"approved": "approved",
"pending": "pending",
"rejected": "rejected"
"rejected": "rejected",
"approved_singular": "approved",
"rejected_singular": "rejected"
},
"errors": {
"generic": "Error loading your hours"
Expand Down Expand Up @@ -645,7 +648,8 @@
"form": {
"event": {
"label": "Event",
"placeholder": "Choose an event"
"placeholder": "Choose an event",
"none": "No event"
},
"task": {
"label": "Task",
Expand Down
8 changes: 6 additions & 2 deletions mobile/src/assets/locales/ro/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -626,11 +626,14 @@
},
"add": "Adaugă ore",
"add_short": "Adaugă",
"sent_toast": "{{n}}h trimise spre aprobare",
"at": "La {{name}}",
"totals": {
"approved": "aprobate",
"pending": "în așteptare",
"rejected": "respinse"
"rejected": "respinse",
"approved_singular": "aprobată",
"rejected_singular": "respinsă"
},
"errors": {
"generic": "Eroare la încărcarea orelor"
Expand Down Expand Up @@ -668,7 +671,8 @@
"form": {
"event": {
"label": "Eveniment",
"placeholder": "Alege un eveniment"
"placeholder": "Alege un eveniment",
"none": "Fără eveniment"
},
"task": {
"label": "Activitate",
Expand Down
1 change: 1 addition & 0 deletions mobile/src/components/ActivityLogForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const ActivityLogForm = ({
label={t('form.event.label')}
placeholder={t('form.event.placeholder')}
organizationId={userProfile?.activeOrganization?.id as string}
{...(inSheet ? { noneLabel: `${t('form.event.none')}` } : {})}
optional
disabled={isLoading}
/>
Expand Down
19 changes: 14 additions & 5 deletions mobile/src/components/hours/AddHoursSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { haptics } from '../../design/haptics';
import { CloseIcon, ICON_STROKE } from '../../design/icons';
import { Button } from '../../design/components/Button';
import { Sheet } from '../../design/components/Sheet';
import { tight, useEffectiveFontScale } from '../../design/components/Text';
import { SubmitSuccess } from './SubmitSuccess';

/** How long „Trimis spre aprobare” stays before the sheet closes (prototype: 1.7 s). */
Expand Down Expand Up @@ -152,6 +153,8 @@ export const AddHoursSheet = forwardRef<AddHoursSheetHandle, AddHoursSheetProps>
{
activityLog: {
...activityLog,
// „Fără eveniment” clears the event: send none rather than an empty id
eventId: activityLog.eventId || (undefined as unknown as string),
volunteerId: organization?.volunteerId,
},
},
Expand Down Expand Up @@ -214,6 +217,8 @@ export const AddHoursSheet = forwardRef<AddHoursSheetHandle, AddHoursSheetProps>
);

const title = `${t('activity_logs:add')}`;
// `.shead h4` Bricolage 30 on a 30pt line, trimmed so Ă is not cut (design/components/Text)
const titleTrim = tight(30, 1, useEffectiveFontScale(fontScaleCap.display));

return (
<Sheet
Expand All @@ -225,6 +230,8 @@ export const AddHoursSheet = forwardRef<AddHoursSheetHandle, AddHoursSheetProps>
animatedPosition={animatedPosition}
enablePanDownToClose={!guarded && !isLoggingHours}
backdropComponent={renderBackdrop}
// `.done` covers the whole sheet, grabber included
{...(sent ? { handleIndicatorStyle: styles.grabberHidden } : {})}
accessibilityLabel={title}
onDismiss={() => {
if (timer.current) {
Expand All @@ -241,8 +248,8 @@ export const AddHoursSheet = forwardRef<AddHoursSheetHandle, AddHoursSheetProps>
<Text
accessibilityRole="header"
maxFontSizeMultiplier={fontScaleCap.display}
style={styles.title}
numberOfLines={1}
style={[styles.title, titleTrim]}
numberOfLines={2}
>
{title}
</Text>
Expand Down Expand Up @@ -297,6 +304,7 @@ export default AddHoursSheet;

const styles = StyleSheet.create((theme) => ({
fill: { flex: 1 },
grabberHidden: { width: 42, height: 5, opacity: 0 },
scrim: { backgroundColor: theme.colors.scrim },
header: {
flexDirection: 'row',
Expand All @@ -306,11 +314,11 @@ const styles = StyleSheet.create((theme) => ({
paddingBottom: 10,
paddingHorizontal: theme.space.gutter,
},
// `.shead h4`: Bricolage 800 30 beside the 46pt close
title: {
flex: 1,
fontFamily: theme.fontFamily.display,
fontSize: 30,
lineHeight: 33,
includeFontPadding: false,
letterSpacing: -1.05,
color: theme.colors.ink,
},
Expand All @@ -327,6 +335,7 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.space.gutter,
paddingTop: 4,
paddingBottom: 24,
gap: 16,
// `.sbody` gap 14
gap: 14,
},
}));
65 changes: 30 additions & 35 deletions mobile/src/components/hours/HoursSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,13 @@
import React, { useId } from 'react';
import React from 'react';
import { View } from 'react-native';
import Svg, { Defs, Pattern, Rect } from 'react-native-svg';
import { useReducedMotion } from 'react-native-reanimated';
import { StyleSheet } from 'react-native-unistyles';
import { useTranslation } from 'react-i18next';
import { fontScaleCap, palette } from '../../design/tokens';
import { AppText } from '../../design/components/Text';
import { ImpactHexagon } from '../../design/components/ImpactHexagon';

/** 12×12 legend square with navy / white diagonal stripes (pending hours in the hexagon). */
const StripedSquare = () => {
const id = `legend-stripes-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;
return (
<Svg width={12} height={12} viewBox="0 0 12 12">
<Defs>
<Pattern
id={id}
patternUnits="userSpaceOnUse"
width={6}
height={6}
patternTransform="rotate(45)"
>
<Rect x={0} y={0} width={3} height={6} fill={palette.navy} />
<Rect x={3} y={0} width={3} height={6} fill={palette.white} />
</Pattern>
</Defs>
<Rect x={0.5} y={0.5} width={11} height={11} rx={4} fill={`url(#${id})`} />
</Svg>
);
};
import { StripedSquare } from './StripedSquare';
import { useCountTo } from './useCountTo';

type LegendKey = 'approved' | 'pending' | 'rejected';

Expand All @@ -43,18 +23,30 @@ export interface HoursSummaryProps {
rejected: number;
/** change it to empty and refill the hexagon (e.g. after sending new hours) */
refillKey?: number;
/** change it to count the approved hours again from 0 (each focus, after sending) */
replay?: unknown;
}

/**
* The Ore header card (`.hrsHead`): the impact hexagon (approved fill + pending stripes) beside a
* legend of the hours per status in Bricolage 30.
*/
export const HoursSummary = ({ approved, pending, rejected, refillKey = 0 }: HoursSummaryProps) => {
export const HoursSummary = ({
approved,
pending,
rejected,
refillKey = 0,
replay,
}: HoursSummaryProps) => {
const { t } = useTranslation('activity_logs');
const items: { key: LegendKey; value: number }[] = [
{ key: 'approved', value: approved || 0 },
{ key: 'pending', value: pending || 0 },
{ key: 'rejected', value: rejected || 0 },
const reduced = useReducedMotion();
// prototype `counts()`: approved counts from 0 with the refill, pending from the number shown
const approvedShown = useCountTo(approved || 0, { reduced, replay });
const pendingShown = useCountTo(pending || 0, { reduced, fromZero: false });
const items: { key: LegendKey; value: number; shown: number }[] = [
{ key: 'approved', value: approved || 0, shown: approvedShown },
{ key: 'pending', value: pending || 0, shown: pendingShown },
{ key: 'rejected', value: rejected || 0, shown: rejected || 0 },
];
return (
<View style={styles.card} testID="hours-summary">
Expand All @@ -69,8 +61,9 @@ export const HoursSummary = ({ approved, pending, rejected, refillKey = 0 }: Hou
/>
</View>
<View style={styles.legend}>
{items.map(({ key, value }) => {
const label = `${t(`totals.${key}`)}`;
{items.map(({ key, value, shown }) => {
// „1h respinsă”, „42h aprobate”
const label = `${t(value === 1 && key !== 'pending' ? `totals.${key}_singular` : `totals.${key}`)}`;
return (
<View
key={key}
Expand All @@ -83,13 +76,13 @@ export const HoursSummary = ({ approved, pending, rejected, refillKey = 0 }: Hou
style={styles.value}
maxFontSizeMultiplier={fontScaleCap.display}
>
{`${value}h`}
{`${shown}h`}
</AppText>
<View style={styles.key}>
{SWATCH[key] ? (
<View style={[styles.square, { backgroundColor: SWATCH[key]! }]} />
) : (
<StripedSquare />
<StripedSquare period={6} />
)}
<AppText variant="label" color={palette.ink2} style={styles.keyText}>
{label}
Expand All @@ -115,13 +108,15 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.card,
},
legend: { flex: 1, gap: 8 },
// `.legend .v`: Bricolage 800 30, line-height 1 (Bricolage needs ~0.9 in RN to sit as tight)
value: {
fontSize: 30,
lineHeight: 32,
lineHeight: 28,
letterSpacing: -0.9,
fontVariant: ['tabular-nums'],
includeFontPadding: false,
},
key: { flexDirection: 'row', alignItems: 'center', gap: 6 },
square: { width: 12, height: 12, borderRadius: 4 },
keyText: { fontFamily: theme.fontFamily.textBold, fontSize: 12.5, flexShrink: 1 },
keyText: { fontFamily: theme.fontFamily.textBold, fontSize: 12.5, lineHeight: 17, flexShrink: 1 },
}));
Loading
Loading