From 145a04232bfc8dc5e3a22cbbafb1234663b85146 Mon Sep 17 00:00:00 2001 From: Chris Sanden Date: Wed, 22 Apr 2026 16:55:38 +0200 Subject: [PATCH 01/18] added summary report from first timer session --- notes/work-report-timer-2026-04-21.md | 92 +++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 notes/work-report-timer-2026-04-21.md diff --git a/notes/work-report-timer-2026-04-21.md b/notes/work-report-timer-2026-04-21.md new file mode 100644 index 0000000..c8a641c --- /dev/null +++ b/notes/work-report-timer-2026-04-21.md @@ -0,0 +1,92 @@ +# Timer Element Work Report + +## #Overview +This note documents the timer work completed by **Chris Sanden** in the Study-Sprint project. + +The git history shows a dedicated timer commit: +- Commit: `d50301cb04837b196110cea43ff15c0493c5fac2` +- Short hash: `d50301c` +- Author: `Chris Sanden ` +- Date: `2026-04-21` +- Message: `First draft of timer element` +- File added: `app/(tabs)/timer.tsx` +- Branch references at inspection time: `timer`, `origin/timer` + +--- + +## #ImplementedFeatures + +### #TimerTab +Created the first draft of a standalone timer screen: +- Added `app/(tabs)/timer.tsx` +- Implemented the timer as its own tab while the final task-start flow is still planned +- Used React Native and Expo tab routing conventions already present in the project + +--- + +### #DurationSelector +Implemented a horizontal animated selector for timer durations: +- Uses `Animated.FlatList` +- Supports snap scrolling with `snapToInterval` +- Shows selectable durations from `1` to `60` +- Uses scaled and faded text animation so the centered duration is emphasized +- Updates the selected duration when scrolling ends + +--- + +### #TimerAnimation +Implemented the first timer start animation: +- Added a circular start button +- Button fades and moves down after the timer starts +- Timer overlay animates into view +- Timer overlay then animates out based on the selected duration +- Uses `Animated.sequence` and `useNativeDriver` + +--- + +## #UserInterface + +The timer screen includes: +- Full-screen dark background +- Red timer overlay +- Large centered duration numbers +- Circular red start button near the bottom of the screen +- Hidden status bar for a focused timer view + +The visual direction is a simple first draft intended to make the timer interaction testable before deeper integration with tasks. + +--- + +## #PlannedIntegration + +The in-code note describes the intended next step: +- Keep the timer as a separate tab initially +- Later open the timer when a user starts a task +- Replace the current duration-number area with task information such as: + - Task name + - Task description +- Potentially add an animated character or visual element if time allows + +--- + +## #GitEvidence + +The work attributed to Chris is supported by this git log entry: + +```text +d50301c Chris Sanden 2026-04-21 First draft of timer element +``` + +The commit added one new file: + +```text +A app/(tabs)/timer.tsx +``` + +The file was later also touched in commit `cb6368a` by `Teodor` on `2026-04-22` as part of broader UI and routing fixes. The original timer implementation documented here is the `d50301c` commit authored by Chris. + +--- + +## #Conclusion + +Chris implemented the first functional timer draft for the application. The work established a standalone timer tab, duration selection, animated start behavior, and a clear path for later connecting the timer to task-start workflows. From b7d62637e638954e2730ecb48fcf87490ee306c8 Mon Sep 17 00:00:00 2001 From: Chris Sanden Date: Wed, 22 Apr 2026 19:12:34 +0200 Subject: [PATCH 02/18] Fixed bug where timer would count space below nav bar as 'usable' space --- app/(tabs)/timer.tsx | 329 ++++++++++++++++++++++++++++--------------- 1 file changed, 215 insertions(+), 114 deletions(-) diff --git a/app/(tabs)/timer.tsx b/app/(tabs)/timer.tsx index eacca7d..20d94f1 100644 --- a/app/(tabs)/timer.tsx +++ b/app/(tabs)/timer.tsx @@ -4,6 +4,7 @@ import { Dimensions, StatusBar, StyleSheet, + Text, TouchableOpacity, View } from 'react-native'; @@ -18,6 +19,10 @@ const colors = { const timers = [...Array(13).keys()].map((i) => (i === 0 ? 1 : i * 5)); const ITEM_SIZE = width * 0.38; const ITEM_SPACING = (width - ITEM_SIZE) / 2; +const placeholderTask = { + name: 'Read chapter 4', + description: 'Focus on the summary questions and write down anything unclear.', +}; /* Har bare skrevet timeren som en egen tab til å begynne med. @@ -26,136 +31,212 @@ som viser TaskName og Description der tallene står nå Kanskje en animert figur hvis vi får tid */ export default function App() { - const scrollX = React.useRef(new Animated.Value(0)).current; - const [duration, setDuration] = React.useState(timers[0]) - const timerAnimation = React.useRef(new Animated.Value(height)).current - const buttonAnimation = React.useRef(new Animated.Value(0)).current - const animation = React.useCallback(() => { - Animated.sequence([ + const [containerHeight, setContainerHeight] = React.useState(0) + const scrollX = React.useRef(new Animated.Value(0)).current; + const [duration, setDuration] = React.useState(timers[0]) + const [isRunning, setIsRunning] = React.useState(false) + const timerAnimation = React.useRef(new Animated.Value(0)).current + const buttonAnimation = React.useRef(new Animated.Value(0)).current + const taskDetailsAnimation = React.useRef(new Animated.Value(0)).current + const animation = React.useCallback(() => { + if (isRunning) { + return; + } + + setIsRunning(true); + taskDetailsAnimation.setValue(0); + + Animated.sequence([ + Animated.parallel([ Animated.timing(buttonAnimation, { toValue: 1, duration: 300, useNativeDriver: true }), - Animated.timing(timerAnimation, { - toValue: 0, - duration: 300, + Animated.timing(taskDetailsAnimation, { + toValue: 1, + duration: 500, useNativeDriver: true }), - Animated.timing(timerAnimation, { - toValue: height, - duration: duration * 1000, - useNativeDriver: true - }), - ]) .start(() => { + ]), + Animated.timing(timerAnimation, { + toValue: 0, + duration: 300, + useNativeDriver: true + }), + Animated.timing(timerAnimation, { + toValue: containerHeight, + duration: duration * 1000, + useNativeDriver: true + }), + ]) .start(({ finished }) => { + if (!finished) { + setIsRunning(false); + return; + } + + Animated.parallel([ Animated.timing(buttonAnimation, { toValue: 0, duration: 300, useNativeDriver: true - }).start() + }), + Animated.timing(taskDetailsAnimation, { + toValue: 0, + duration: 300, + useNativeDriver: true + }), + ]).start(() => { + setIsRunning(false); }) - }, [duration]) - - const opacity = buttonAnimation.interpolate({ - inputRange: [0, 1], - outputRange: [1, 0] }) - const translateY = buttonAnimation.interpolate({ - inputRange: [0, 1], - outputRange: [0, 200] + }, [buttonAnimation, duration, isRunning, taskDetailsAnimation, timerAnimation]) + + React.useEffect(() => { + if (containerHeight > 0) { + timerAnimation.setValue(containerHeight); + } }) - - return ( - - + + {placeholderTask.name} + {placeholderTask.description} + ); } @@ -176,5 +257,25 @@ const styles = StyleSheet.create({ fontFamily: 'Menlo', color: colors.text, fontWeight: '900', + }, + taskDetails: { + position: 'absolute', + top: height * 0.56, + left: 32, + right: 32, + alignItems: 'center', + }, + taskName: { + color: colors.text, + fontSize: 24, + fontWeight: '800', + textAlign: 'center', + }, + taskDescription: { + color: colors.text, + fontSize: 16, + lineHeight: 22, + marginTop: 10, + textAlign: 'center', } -}); \ No newline at end of file +}); From 1879c7c1f7a6578022499a15ed1ed718ff4c84bd Mon Sep 17 00:00:00 2001 From: Chris Sanden Date: Wed, 22 Apr 2026 20:41:19 +0200 Subject: [PATCH 03/18] Added placeholder task info and made some UI/UX improvements --- app/(tabs)/timer.tsx | 110 +++++++++++++++++++++++++++++++------------ 1 file changed, 79 insertions(+), 31 deletions(-) diff --git a/app/(tabs)/timer.tsx b/app/(tabs)/timer.tsx index 20d94f1..fceb963 100644 --- a/app/(tabs)/timer.tsx +++ b/app/(tabs)/timer.tsx @@ -19,32 +19,55 @@ const colors = { const timers = [...Array(13).keys()].map((i) => (i === 0 ? 1 : i * 5)); const ITEM_SIZE = width * 0.38; const ITEM_SPACING = (width - ITEM_SIZE) / 2; +const TIMER_UNIT_IN_SECONDS = 1; // Set to 60 for timer value to represent minutes const placeholderTask = { name: 'Read chapter 4', description: 'Focus on the summary questions and write down anything unclear.', }; -/* -Har bare skrevet timeren som en egen tab til å begynne med. -Planen er at når bruker starter en task så vil de få opp denne timeren -som viser TaskName og Description der tallene står nå -Kanskje en animert figur hvis vi får tid -*/ +function formatTime(totalSeconds: number) { + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + return `${minutes.toString().padStart(2, '0')}:${ + seconds.toString().padStart(2, '0')}`; +} + export default function App() { const [containerHeight, setContainerHeight] = React.useState(0) - const scrollX = React.useRef(new Animated.Value(0)).current; const [duration, setDuration] = React.useState(timers[0]) const [isRunning, setIsRunning] = React.useState(false) + const [timeRemaining, setTimeRemaining] = React.useState(0) + const [selectedIndex, setSelectedIndex] = React.useState(0) + const [showCountdownText, setShowCountdownText] = React.useState(false) + const scrollX = React.useRef(new Animated.Value(0)).current; const timerAnimation = React.useRef(new Animated.Value(0)).current const buttonAnimation = React.useRef(new Animated.Value(0)).current const taskDetailsAnimation = React.useRef(new Animated.Value(0)).current + const countdownAnimation = React.useRef(new Animated.Value(0)).current + const animation = React.useCallback(() => { - if (isRunning) { + if (isRunning || containerHeight === 0) { return; } setIsRunning(true); + setShowCountdownText(true); taskDetailsAnimation.setValue(0); + countdownAnimation.setValue(0); + + const totalSeconds = duration * TIMER_UNIT_IN_SECONDS; + setTimeRemaining(totalSeconds); + + const countdown = setInterval(() => { + setTimeRemaining((currentTime) => { + if (currentTime <= 1) { + clearInterval(countdown) + return 0; + } + return currentTime -1; + }); + }, 1000); Animated.sequence([ Animated.parallel([ @@ -58,6 +81,11 @@ export default function App() { duration: 500, useNativeDriver: true }), + Animated.timing(countdownAnimation, { + toValue: 1, + duration: 300, + useNativeDriver: true + }), ]), Animated.timing(timerAnimation, { toValue: 0, @@ -66,37 +94,45 @@ export default function App() { }), Animated.timing(timerAnimation, { toValue: containerHeight, - duration: duration * 1000, + duration: totalSeconds * 1000, useNativeDriver: true - }), - ]) .start(({ finished }) => { + }) + ]).start(({ finished }) => { if (!finished) { setIsRunning(false); return; } + Animated.timing(countdownAnimation, { + toValue: 0, + duration: 200, + useNativeDriver: true + }).start(() => { + setShowCountdownText(false); + Animated.parallel([ - Animated.timing(buttonAnimation, { - toValue: 0, - duration: 300, - useNativeDriver: true - }), - Animated.timing(taskDetailsAnimation, { - toValue: 0, - duration: 300, - useNativeDriver: true - }), - ]).start(() => { - setIsRunning(false); - }) + Animated.timing(buttonAnimation, { + toValue: 0, + duration: 300, + useNativeDriver: true + }), + Animated.timing(taskDetailsAnimation, { + toValue: 0, + duration: 300, + useNativeDriver: true + }), + ]).start(() => { + setIsRunning(false); + }) }) - }, [buttonAnimation, duration, isRunning, taskDetailsAnimation, timerAnimation]) + }) +}, [countdownAnimation, buttonAnimation, containerHeight, duration, isRunning, taskDetailsAnimation, timerAnimation]) React.useEffect(() => { - if (containerHeight > 0) { + if (containerHeight > 0 && !isRunning) { timerAnimation.setValue(containerHeight); } - }) + }, [containerHeight, isRunning, timerAnimation]) const opacity = buttonAnimation.interpolate({ inputRange: [0, 1], @@ -127,7 +163,7 @@ return (