Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Timer for Duration Type Goals #83

Merged
merged 29 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4c54894
test commit
2-Chengs Oct 11, 2024
618d806
Removed empty lines of test commit
2-Chengs Oct 11, 2024
a838d2f
implemented startGoal page and functionality
2-Chengs Oct 11, 2024
f3c7e73
Created goalLog table in schema and convex functions
2-Chengs Oct 13, 2024
27863dc
Created goalLogs table in schema and convex functions
2-Chengs Oct 13, 2024
ec54bed
removed userId from goalLogs schema
2-Chengs Oct 13, 2024
7449814
implemented deleteGoalandGoalLogs crud operation
2-Chengs Oct 13, 2024
837a4b7
added unitsCompleted field to goalLogs
2-Chengs Oct 13, 2024
07b39d7
deleted utils file
2-Chengs Oct 13, 2024
f130fe0
Added weeks into goalFormStore and weeks selector on the create goal …
2-Chengs Oct 13, 2024
9e55196
added weeks into schema for goals and created goalLog generation on c…
2-Chengs Oct 13, 2024
1faae2f
Changed routes to capture both goalId and goalLogId and created back …
2-Chengs Oct 13, 2024
c147df1
Added share screen after completing goal
2-Chengs Oct 13, 2024
d3ae299
fixed text bug on share screen
2-Chengs Oct 13, 2024
ba843df
fixed bug in deleteGoalAndGoalLogs
2-Chengs Oct 13, 2024
689dc7f
fixed bug in complete screen
2-Chengs Oct 13, 2024
cfa2ca2
added type to import statement for id
2-Chengs Oct 13, 2024
84a6183
fixed error in edit page
2-Chengs Oct 13, 2024
63fc892
Implemented timer functionality and buttons for Duration type goals
bli129 Oct 14, 2024
b2e4424
Goal progress display reduced to 2 decimal places if not clean integer
bli129 Oct 14, 2024
0cfd28a
Removed console logs
bli129 Oct 14, 2024
bb3a459
Added timer dependencies
bli129 Oct 14, 2024
d3c4d12
Wrapped timer functions in callbacks
bli129 Oct 14, 2024
dd08cc9
Added missing dependencies and JSX checks
bli129 Oct 14, 2024
a523ee5
Removed router dependency and refactored timer button color conditions
bli129 Oct 14, 2024
21258c1
Fixed eslint error
bli129 Oct 14, 2024
aafec42
Added Timer button for duration based goals that redirect to start pa…
bli129 Oct 15, 2024
896fe68
Added timer support for unit type General -> minutes
bli129 Oct 15, 2024
33b4de7
eslint and prettier fixes
bli129 Oct 15, 2024
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
117 changes: 84 additions & 33 deletions app/(tabs)/goals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SafeAreaView } from "react-native-safe-area-context";

import { Text } from "~/components/ui/text";
import { Button } from "~/components/ui/button";
import { Link, SplashScreen } from "expo-router";
import { Link, router, SplashScreen } from "expo-router";
import {
useEffect,
useRef,
Expand All @@ -29,13 +29,33 @@ import { GOAL_ICONS } from "~/constants/goal-icons";
export default function GoalsPage() {
const { today, tomorrow, yesterday } = getTodayYesterdayTomorrow();
const [selectedDate, setSelectedDate] = useState(today);

// Fetch both goals and goalLogs
const goals = useQuery(api.goals.listGoals);
const goalLogs = useQuery(api.goalLogs.listGoalLogs);

useEffect(() => {
if (goals) {
if (goals && goalLogs) {
SplashScreen.hideAsync();
}
}, [goals]);
}, [goals, goalLogs]);

// Filter goalLogs for the selectedDate
const filteredGoalLogs = goalLogs
? goalLogs.filter((log) => {
const logDate = new Date(log.date).setHours(0, 0, 0, 0); // Compare at date level
const selectedDateStart = new Date(selectedDate).setHours(0, 0, 0, 0);
return logDate === selectedDateStart;
})
: [];

// Match filtered goalLogs to their corresponding goals
const matchedGoals = filteredGoalLogs
.map((log) => {
const goal = goals?.find((goal) => goal._id === log.goalId);
return goal ? { goal, goalLog: log } : null;
})
.filter((item) => item !== null); // Remove nulls

return (
<SafeAreaView
Expand Down Expand Up @@ -68,7 +88,7 @@ export default function GoalsPage() {
>
Goals
</Text>
{!goals ? (
{!goals || !goalLogs ? (
<View className="mt-10 flex flex-row justify-center gap-2">
<Text>Loading goals...</Text>
<ActivityIndicator />
Expand All @@ -79,15 +99,17 @@ export default function GoalsPage() {
paddingBottom: 60,
}}
className="mt-6 border-t border-t-[#fff]/10 pt-6"
data={goals}
data={matchedGoals}
ItemSeparatorComponent={() => (
<View className="my-4 ml-14 mr-6 h-0.5 bg-[#fff]/10" />
)}
ListEmptyComponent={() => (
<Text className="text-center">No goals found.</Text>
<Text className="text-center">No goals found for this date.</Text>
)}
renderItem={({ item }) => <GoalItem goal={item} />}
keyExtractor={(goal) => goal._id.toString()}
renderItem={({ item }) => (
<GoalItem goal={item.goal} goalLog={item.goalLog} />
)}
keyExtractor={(item) => item.goal._id.toString()}
/>
)}
</View>
Expand All @@ -109,40 +131,69 @@ export default function GoalsPage() {

interface GoalItemProps {
goal: Doc<"goals">;
goalLog: Doc<"goalLogs">;
}

function GoalItem({ goal }: GoalItemProps) {
function GoalItem({ goal, goalLog }: GoalItemProps) {
const IconComp = GOAL_ICONS.find(
(item) => item.name === goal.selectedIcon
)?.component;

const handleTimerRedirect = () => {
router.push(`/goals/${goal._id}/${goalLog._id}/start`);
};

const AlarmIconComp = GOAL_ICONS.find(
(icon) => icon.name === "alarm"
)?.component;

return (
<Link href={`/goals/${goal._id}`} asChild>
<Pressable>
<View className="flex-row items-center gap-4">
<View
className={cn(
"items-center justify-center rounded-full bg-[#299240]/20 p-1"
)}
>
<IconComp
name={goal.selectedIcon}
color={goal.selectedIconColor}
size={32}
/>
</View>
<View className="flex-row items-center gap-4">
<Link href={`/goals/${goal._id}/${goalLog._id}`} asChild>
<Pressable className="flex-1">
<View className="flex-row items-center gap-4">
<View
className={cn(
"items-center justify-center rounded-full bg-[#299240]/20 p-1"
)}
>
<IconComp
name={goal.selectedIcon}
color={goal.selectedIconColor}
size={32}
/>
</View>

<View className="w-full gap-2">
<Text style={{ fontFamily: fontFamily.openSans.medium }}>
{goal.name}
</Text>
<Text className="text-xs text-muted-foreground">
{`0 / ${goal.unitValue} ${goal.unit}`}
</Text>
<View className="w-full gap-2">
<Text style={{ fontFamily: fontFamily.openSans.medium }}>
{goal.name}
</Text>
<Text className="text-xs text-muted-foreground">
{`${Math.floor(goalLog.unitsCompleted)} / ${Math.floor(goal.unitValue)} ${goal.unit}`}
</Text>
</View>
</View>
</View>
</Pressable>
</Link>
</Pressable>
</Link>

{(goal.unitType === "Duration" || goal.unit === "minutes") && (
<Pressable
onPress={handleTimerRedirect}
className="flex-row items-center justify-center rounded-full bg-gray-600 p-2"
style={{ paddingHorizontal: 12 }}
>
{!!AlarmIconComp && (
<AlarmIconComp name="alarm" size={16} color="#fff" />
)}
<Text
className="ml-2 text-base text-white"
style={{ fontFamily: fontFamily.openSans.bold }}
>
Timer
</Text>
</Pressable>
)}
</View>
);
}

Expand Down
88 changes: 88 additions & 0 deletions app/goals/[goalId]/[goalLogId]/complete/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useQuery } from "convex/react";
import { Stack, useLocalSearchParams, router } from "expo-router";
import { Pressable, View, Share } from "react-native";
import { Text } from "~/components/ui/text";
import { api } from "~/convex/_generated/api";
import { fontFamily } from "~/lib/font";
import type { Id } from "~/convex/_generated/dataModel";

export default function GoalCompletionScreen() {
const { goalId } = useLocalSearchParams<{ goalId: Id<"goals"> }>();

// Fetch all goalLogs associated with this goalId
const goalLogs = useQuery(api.goalLogs.getGoalLogsbyGoalId, { goalId });

if (!goalLogs) {
return <Text>Loading...</Text>;
}

// Calculate total, completed, and remaining goalLogs
const totalLogs = goalLogs.length;
const completedLogs = goalLogs.filter((log) => log.isComplete).length;

const handleShare = async () => {
try {
await Share.share({
message: `I just completed ${completedLogs} day(s) out of ${totalLogs} towards my goal! #goals`,
});
} catch (error) {
console.error("Error sharing:", error);
}
};

return (
<View className="h-full items-center justify-center p-4">
<Stack.Screen
options={{
headerStyle: {
backgroundColor: "#0b1a28",
},
headerTintColor: "#fff",
headerTitle: () => (
<Text
className="text-xl"
style={{ fontFamily: fontFamily.openSans.bold }}
>
Congratulations!
</Text>
),
headerBackTitleVisible: false,
}}
/>
<Text
className="text-2xl text-white"
style={{ fontFamily: fontFamily.openSans.bold }}
>
Congratulations!
</Text>
<Text className="mt-4 text-lg text-white">
You have completed {completedLogs} day(s) out of {totalLogs} towards
your goal.
</Text>

<Pressable
className="mt-6 w-full items-center rounded-lg bg-blue-600 p-4"
onPress={handleShare}
>
<Text
className="text-lg text-white"
style={{ fontFamily: fontFamily.openSans.bold }}
>
Share Your Progress
</Text>
</Pressable>

<Pressable
className="mt-4 w-full items-center rounded-lg bg-green-600 p-4"
onPress={() => router.navigate("/goals")}
>
<Text
className="text-lg text-white"
style={{ fontFamily: fontFamily.openSans.bold }}
>
Back to Goals
</Text>
</Pressable>
</View>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,31 @@ import { api } from "~/convex/_generated/api";
import type { Id } from "~/convex/_generated/dataModel";
import { fontFamily } from "~/lib/font";
import * as DropdownMenu from "zeego/dropdown-menu";
import { useState, useEffect } from "react";

export default function GoalScreen() {
const { goalId } = useLocalSearchParams<{ goalId: Id<"goals"> }>();
const { goalId, goalLogId } = useLocalSearchParams<{
goalId: Id<"goals">;
goalLogId: Id<"goalLogs">;
}>();

const goal = useQuery(api.goals.getGoalById, { goalId });
const deleteGoal = useMutation(api.goals.deleteGoal);
const goalLog = useQuery(api.goalLogs.getGoalLogById, { goalLogId });
const goalLogs = useQuery(api.goalLogs.getGoalLogsbyGoalId, { goalId });

const deleteGoalAndGoalLogs = useMutation(api.goals.deleteGoalAndGoalLogs);

const [progress, setProgress] = useState<number>(0);

useEffect(() => {
if (goalLogs) {
const totalLogs = goalLogs.length;
const completedLogs = goalLogs.filter((log) => log.isComplete).length;

const percentage = totalLogs > 0 ? (completedLogs / totalLogs) * 100 : 0;
setProgress(percentage);
}
}, [goalLogs]);

const handleDelete = async () => {
Alert.alert(
Expand All @@ -21,7 +41,7 @@ export default function GoalScreen() {
{
text: "Yes",
onPress: async () => {
await deleteGoal({ goalId });
await deleteGoalAndGoalLogs({ goalId });
router.dismiss();
},
style: "destructive",
Expand All @@ -31,6 +51,13 @@ export default function GoalScreen() {
);
};

const handleStartGoal = () => {
router.push({
pathname: "/goals/[goalId]/[goalLogId]/start",
params: { goalId, goalLogId },
});
};

return (
<View className="h-full gap-4 bg-background p-4">
<Stack.Screen
Expand All @@ -55,12 +82,7 @@ export default function GoalScreen() {
<FontAwesome5 name="ellipsis-h" size={20} color="#fff" />
</Pressable>
</DropdownMenu.Trigger>
<DropdownMenu.Content
key="actions"
placeholder=""
onPointerEnterCapture={() => {}}
onPointerLeaveCapture={() => {}}
>
<DropdownMenu.Content key="actions" placeholder="">
<DropdownMenu.Item
onSelect={() =>
router.navigate({
Expand All @@ -70,37 +92,49 @@ export default function GoalScreen() {
}
key="edit-goal"
textValue="Edit Goal"
placeholder=""
onPointerEnterCapture={() => {}}
onPointerLeaveCapture={() => {}}
>
<DropdownMenu.ItemIcon
ios={{
name: "pencil.line",
}}
/>
<DropdownMenu.ItemIcon ios={{ name: "pencil.line" }} />
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={handleDelete}
destructive
key="delete-goal"
textValue="Delete Goal"
placeholder=""
onPointerEnterCapture={() => {}}
onPointerLeaveCapture={() => {}}
>
<DropdownMenu.ItemIcon
ios={{
name: "trash",
}}
/>
<DropdownMenu.ItemIcon ios={{ name: "trash" }} />
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
),
}}
/>

<View className="my-4 rounded-lg bg-gray-700 p-4">
<Text
className="text-base text-white"
style={{ fontFamily: fontFamily.openSans.medium }}
>
Progress: {progress.toFixed(2)}%
</Text>
<Text className="text-sm text-gray-400">
You have completed {goalLogs?.filter((log) => log.isComplete).length}{" "}
of {goalLogs?.length} logs.
</Text>
</View>

<Pressable
className={`mt-5 items-center rounded-lg p-3 ${goalLog?.isComplete ? "bg-gray-400" : "bg-[#299240]"}`}
onPress={goalLog?.isComplete ? null : handleStartGoal} // Disable press if goalLog is complete
disabled={goalLog?.isComplete} // Disable the button if the goalLog is complete
>
<Text
className="text-base text-white"
style={{ fontFamily: fontFamily.openSans.bold }}
>
{goalLog?.isComplete ? "Goal Log Completed" : "Start Goal"}
</Text>
</Pressable>
</View>
);
}
Loading
Loading