Skip to content

Commit 3509c34

Browse files
feat: add Trail Condition Reporting System
- Add trailConditionReports table to DB schema with surface, condition, hazards, water crossings fields - Create DB migration SQL (0033_trail_conditions.sql) - Create API routes for trail conditions (list, create, list-mine, delete) - Register trail conditions routes in API index - Add enableTrailConditions feature flag to config.ts - Create trail-conditions feature module with types, store, hooks, components - Offline-first store using Legend State + SQLite persistence - SubmitConditionReportForm, TrailConditionReportCard, ConditionBadge components - Replace static mock data in trail-conditions screen with real API data - Add submit report modal to trail conditions screen - Add post-hike condition report prompt to trip detail screen - Add i18n translations for trail conditions Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com>
1 parent 8100c1d commit 3509c34

File tree

20 files changed

+1089
-173
lines changed

20 files changed

+1089
-173
lines changed
Lines changed: 76 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,71 @@
1-
import { LargeTitleHeader, Text } from '@packrat/ui/nativewindui';
2-
import { cn } from 'expo-app/lib/cn';
3-
import { ScrollView, View } from 'react-native';
1+
import { ActivityIndicator, LargeTitleHeader, Text } from '@packrat/ui/nativewindui';
2+
import { featureFlags } from 'expo-app/config';
3+
import { SubmitConditionReportForm } from 'expo-app/features/trail-conditions/components/SubmitConditionReportForm';
4+
import { TrailConditionReportCard } from 'expo-app/features/trail-conditions/components/TrailConditionReportCard';
5+
import { useTrailConditionReports } from 'expo-app/features/trail-conditions/hooks/useTrailConditionReports';
6+
import { useState } from 'react';
7+
import { Modal, Pressable, ScrollView, View } from 'react-native';
48

5-
// Mock data for trail conditions
6-
const TRAIL_CONDITIONS = [
7-
{
8-
id: '1',
9-
section: 'Springer Mountain to Neels Gap',
10-
state: 'GA',
11-
lastUpdated: '2 days ago',
12-
condition: 'Good',
13-
details:
14-
'Trail is well maintained with clear blazes. Some muddy sections after recent rain but passable. Water sources are flowing well.',
15-
reports: [
16-
{
17-
user: 'HikerJohn',
18-
date: 'May 10',
19-
text: 'Trail in great shape. Saw some trail maintenance crews working near Blood Mountain.',
20-
},
21-
{
22-
user: 'MountainGoat',
23-
date: 'May 8',
24-
text: 'Muddy near stream crossings but otherwise good. All water sources flowing.',
25-
},
26-
],
27-
},
28-
{
29-
id: '2',
30-
section: 'Neels Gap to Unicoi Gap',
31-
state: 'GA',
32-
lastUpdated: '5 days ago',
33-
condition: 'Fair',
34-
details:
35-
'Some blowdowns reported between Low Gap and Blue Mountain shelters. Rocky sections can be slippery when wet. Moderate difficulty.',
36-
reports: [
37-
{
38-
user: 'TrailAngel22',
39-
date: 'May 7',
40-
text: 'Three large trees down about 2 miles north of Low Gap shelter. Passable but difficult.',
41-
},
42-
{
43-
user: 'ThruHiker2024',
44-
date: 'May 5',
45-
text: 'Rocky sections are challenging in rain. Trekking poles recommended.',
46-
},
47-
],
48-
},
49-
{
50-
id: '3',
51-
section: 'Unicoi Gap to Tray Mountain',
52-
state: 'GA',
53-
lastUpdated: '1 week ago',
54-
condition: 'Excellent',
55-
details:
56-
'Recently maintained trail with clear path and blazes. Some steep sections but well-graded. All water sources reliable.',
57-
reports: [
58-
{
59-
user: 'MountainLover',
60-
date: 'May 4',
61-
text: 'Trail is in excellent condition. Views from Rocky Mountain are spectacular!',
62-
},
63-
{
64-
user: 'GearTester',
65-
date: 'May 2',
66-
text: 'Easy to follow trail with good camping spots near Tray Mountain shelter.',
67-
},
68-
],
69-
},
70-
{
71-
id: '4',
72-
section: "Tray Mountain to Dick's Creek Gap",
73-
state: 'GA',
74-
lastUpdated: '10 days ago',
75-
condition: 'Poor',
76-
details:
77-
'Multiple blowdowns and washouts reported after recent storms. Some trail reroutes in effect. Check with local rangers for updates.',
78-
reports: [
79-
{
80-
user: 'SectionHiker',
81-
date: 'April 30',
82-
text: 'Difficult hiking with many obstacles. Several trees down across trail.',
83-
},
84-
{
85-
user: 'TrailRunner',
86-
date: 'April 28',
87-
text: 'Trail badly eroded in places. Slow going and requires careful navigation.',
88-
},
89-
],
90-
},
91-
];
92-
93-
function ConditionBadge({ condition }: { condition: string }) {
94-
const getColor = () => {
95-
switch (condition) {
96-
case 'Excellent':
97-
return 'bg-green-500';
98-
case 'Good':
99-
return 'bg-blue-500';
100-
case 'Fair':
101-
return 'bg-amber-500';
102-
case 'Poor':
103-
return 'bg-red-500';
104-
default:
105-
return 'bg-gray-500';
106-
}
107-
};
9+
export default function TrailConditionsScreen() {
10+
const [showSubmitForm, setShowSubmitForm] = useState(false);
11+
const { data: reports, isLoading, error } = useTrailConditionReports();
10812

109-
return (
110-
<View className={cn('rounded-full px-2 py-1', getColor())}>
111-
<Text variant="caption2" className="font-medium text-white">
112-
{condition}
113-
</Text>
114-
</View>
115-
);
116-
}
13+
if (!featureFlags.enableTrailConditions) return null;
11714

118-
function TrailConditionCard({ trail }: { trail: (typeof TRAIL_CONDITIONS)[0] }) {
119-
return (
120-
<View className="mx-4 mb-3 overflow-hidden rounded-xl bg-card shadow-sm">
121-
<View className="border-b border-border p-4">
122-
<View className="flex-row items-center justify-between">
123-
<Text variant="heading" className="flex-1 font-semibold">
124-
{trail.section}
125-
</Text>
126-
<ConditionBadge condition={trail.condition} />
127-
</View>
128-
<Text variant="subhead" className="mt-1 text-muted-foreground">
129-
{trail.state} • Updated {trail.lastUpdated}
130-
</Text>
131-
</View>
132-
133-
<View className="p-4">
134-
<Text variant="body" className="mb-3">
135-
{trail.details}
136-
</Text>
137-
138-
<View className="mt-2">
139-
<Text variant="subhead" className="mb-2 font-medium">
140-
Recent Reports:
141-
</Text>
142-
{trail.reports.map((report) => (
143-
<View key={report.text} className="mb-2 rounded-md bg-muted p-3 dark:bg-gray-50/10">
144-
<View className="flex-row items-center justify-between">
145-
<Text variant="footnote" className="font-medium">
146-
{report.user}
147-
</Text>
148-
<Text variant="caption1" className="text-muted-foreground">
149-
{report.date}
150-
</Text>
151-
</View>
152-
<Text variant="footnote" className="mt-1">
153-
{report.text}
154-
</Text>
155-
</View>
156-
))}
157-
</View>
158-
</View>
159-
</View>
160-
);
161-
}
162-
163-
export default function TrailConditionsScreen() {
16415
return (
16516
<>
166-
<LargeTitleHeader title="Trail Conditions" />
17+
<LargeTitleHeader
18+
title="Trail Conditions"
19+
rightView={() => (
20+
<Pressable
21+
onPress={() => setShowSubmitForm(true)}
22+
className="mr-2 rounded-full bg-primary px-3 py-1.5"
23+
>
24+
<Text variant="footnote" className="font-semibold text-primary-foreground">
25+
+ Report
26+
</Text>
27+
</Pressable>
28+
)}
29+
/>
30+
16731
<ScrollView className="flex-1">
16832
<View className="p-4">
16933
<Text variant="subhead" className="mb-2 text-muted-foreground">
17034
Current trail conditions from recent hiker reports
17135
</Text>
17236
</View>
17337

174-
<View className="pb-4">
175-
{TRAIL_CONDITIONS.map((trail) => (
176-
<TrailConditionCard key={trail.id} trail={trail} />
177-
))}
178-
</View>
38+
{isLoading ? (
39+
<View className="flex-1 items-center justify-center py-12">
40+
<ActivityIndicator />
41+
</View>
42+
) : error ? (
43+
<View className="mx-4 mb-3 rounded-xl bg-card p-4">
44+
<Text variant="body" className="text-center text-muted-foreground">
45+
Unable to load trail conditions. Pull to refresh.
46+
</Text>
47+
</View>
48+
) : reports && reports.length > 0 ? (
49+
<View className="pb-4">
50+
{reports.map((report) => (
51+
<TrailConditionReportCard key={report.id} report={report} />
52+
))}
53+
</View>
54+
) : (
55+
<View className="mx-4 mb-3 rounded-xl bg-card p-8">
56+
<Text variant="body" className="text-center text-muted-foreground">
57+
No trail condition reports yet. Be the first to report!
58+
</Text>
59+
<Pressable
60+
onPress={() => setShowSubmitForm(true)}
61+
className="mt-4 rounded-lg bg-primary px-4 py-3"
62+
>
63+
<Text className="text-center font-semibold text-primary-foreground">
64+
Submit a Report
65+
</Text>
66+
</Pressable>
67+
</View>
68+
)}
17969

18070
<View className="mx-4 my-2 mb-6 rounded-lg bg-card p-4">
18171
<View className="rounded-md bg-muted p-3 dark:bg-gray-50/10">
@@ -186,6 +76,26 @@ export default function TrailConditionsScreen() {
18676
</View>
18777
</View>
18878
</ScrollView>
79+
80+
{/* Submit Report Modal */}
81+
<Modal
82+
visible={showSubmitForm}
83+
animationType="slide"
84+
presentationStyle="pageSheet"
85+
onRequestClose={() => setShowSubmitForm(false)}
86+
>
87+
<View className="flex-1 bg-background">
88+
<View className="flex-row items-center justify-between border-b border-border px-4 py-3">
89+
<Text variant="heading" className="font-semibold">
90+
Report Trail Conditions
91+
</Text>
92+
<Pressable onPress={() => setShowSubmitForm(false)}>
93+
<Text className="font-semibold text-primary">Cancel</Text>
94+
</Pressable>
95+
</View>
96+
<SubmitConditionReportForm onSuccess={() => setShowSubmitForm(false)} />
97+
</View>
98+
</Modal>
18999
</>
190100
);
191101
}

apps/expo/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export const featureFlags = {
55
enableShoppingList: false,
66
enableSharedPacks: false,
77
enablePackTemplates: true,
8+
enableTrailConditions: true,
89
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Text } from '@packrat/ui/nativewindui';
2+
import { cn } from 'expo-app/lib/cn';
3+
import { View } from 'react-native';
4+
import type { OverallCondition } from '../types';
5+
6+
interface ConditionBadgeProps {
7+
condition: OverallCondition | string;
8+
}
9+
10+
export function ConditionBadge({ condition }: ConditionBadgeProps) {
11+
const getColor = () => {
12+
switch (condition) {
13+
case 'excellent':
14+
return 'bg-green-500';
15+
case 'good':
16+
return 'bg-blue-500';
17+
case 'fair':
18+
return 'bg-amber-500';
19+
case 'poor':
20+
return 'bg-red-500';
21+
default:
22+
return 'bg-gray-500';
23+
}
24+
};
25+
26+
const getLabel = () => {
27+
return condition.charAt(0).toUpperCase() + condition.slice(1);
28+
};
29+
30+
return (
31+
<View className={cn('rounded-full px-2 py-1', getColor())}>
32+
<Text variant="caption2" className="font-medium text-white">
33+
{getLabel()}
34+
</Text>
35+
</View>
36+
);
37+
}

0 commit comments

Comments
 (0)