Merge pull request #1 from Fhj0607/timer

Timer
This commit is contained in:
Chris Sanden
2026-04-22 14:37:50 +02:00
committed by GitHub
13 changed files with 1177 additions and 226 deletions

View File

@@ -6,7 +6,7 @@ This explains how to run the app with Expo.
## Requirements ## Requirements
- Node.js - Node.js 20.19.4 or newer
- npm - npm
- Expo CLI - Expo CLI
- Android Studio with an emulator OR Expo Go on a phone - Android Studio with an emulator OR Expo Go on a phone

View File

@@ -3,6 +3,7 @@ import { Session } from "@supabase/supabase-js";
import { Tabs } from "expo-router"; import { Tabs } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function TabLayout() { export default function TabLayout() {
const [session, SetSession] = useState<Session | null>(null) const [session, SetSession] = useState<Session | null>(null)
const [loading, SetLoading] = useState(true); const [loading, SetLoading] = useState(true);

View File

@@ -1,4 +1,3 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { import {
router, router,
@@ -15,11 +14,10 @@ import {
Platform, Platform,
Pressable, Pressable,
ScrollView, ScrollView,
StyleSheet,
Text, Text,
TextInput, TextInput,
TouchableWithoutFeedback, TouchableWithoutFeedback,
View View,
} from 'react-native'; } from 'react-native';
export default function EditTask() { export default function EditTask() {
@@ -66,84 +64,103 @@ export default function EditTask() {
}, [taskId]) }, [taskId])
); );
const handleSaveTask = async () => { const handleSaveTask = async () => {
if (!title.trim() || !description.trim() || !deadline.trim()) { if (!title.trim() || !description.trim() || !deadline.trim()) {
Alert.alert("All fields are required!"); Alert.alert('All fields are required!');
return; return;
} }
setIsSaving(true); setIsSaving(true);
try { try {
const { data: userData, error: userError } = await supabase.auth.getUser(); const { data: userData, error: userError } = await supabase.auth.getUser();
if (userError || !userData.user) { if (userError || !userData.user) {
router.replace("../createUser"); router.replace('../createUser');
return; return;
} }
const { error: dbError } = await supabase.from("tasks").update({
title, const { error: dbError } = await supabase
description, .from('tasks')
isCompleted, .update({
lastChanged: new Date().toISOString(), title: title.trim(),
deadline, description: description.trim(),
uId: userData.user.id, isCompleted,
}).eq("tId", taskId); lastChanged: new Date().toISOString(),
deadline: deadline.trim(),
uId: userData.user.id,
})
.eq('tId', taskId);
if (dbError) { if (dbError) {
Alert.alert("Task could not be edited, please try again"); Alert.alert('Task could not be edited, please try again');
return; return;
} }
Alert.alert("Task successfully edited!");
Alert.alert('Task successfully edited!');
router.back(); router.back();
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
}; };
return ( return (
<> <>
<Stack.Screen <Stack.Screen
options={{ options={{
title: "Edit Task", title: 'Edit Task',
headerTitleStyle: defaultStyles.title headerTitleStyle: {
fontSize: 20,
fontWeight: '700',
},
}} }}
/> />
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} className="flex-1 bg-gray-100"
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
> >
<TouchableWithoutFeedback onPress={Keyboard.dismiss}> <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<ScrollView <ScrollView
style={defaultStyles.container} keyboardShouldPersistTaps="handled"
keyboardShouldPersistTaps="handled" contentContainerClassName="flex-grow justify-center px-5 py-8"
> >
<View style={styles.card}> <View className="rounded-3xl bg-white p-6 shadow-lg">
<Text style={defaultStyles.title}>Edit Task</Text> <Text className="mb-1 text-3xl font-bold text-gray-900">
<Text style={styles.subtitle}>Update your task details below.</Text> Edit Task
</Text>
<Text className="mb-6 text-base text-gray-500">
Update the details for this task.
</Text>
{isLoading ? ( {isLoading ? (
<View style={styles.loadingContainer}> <View className="items-center justify-center py-12">
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
<Text style={styles.loadingText}>Loading task...</Text> <Text className="mt-3 text-gray-500">Loading task...</Text>
</View> </View>
) : ( ) : (
<> <>
<View style={styles.field}> <View className="mb-4">
<Text style={styles.label}>Title</Text> <Text className="mb-2 text-sm font-semibold text-gray-700">
<TextInput Title
style={styles.input} </Text>
placeholder="Enter task title"
value={title}
onChangeText={setTitle}
/>
</View>
<View style={styles.field}>
<Text style={styles.label}>Description</Text>
<TextInput <TextInput
style={[styles.input, styles.textArea]} className="rounded-xl border border-gray-300 bg-gray-50 px-4 py-3 text-base text-gray-900"
placeholder="Enter task description" placeholder="Enter title"
placeholderTextColor="#9ca3af"
value={title}
onChangeText={setTitle}
/>
</View>
<View className="mb-4">
<Text className="mb-2 text-sm font-semibold text-gray-700">
Description
</Text>
<TextInput
className="min-h-28 rounded-xl border border-gray-300 bg-gray-50 px-4 py-3 text-base text-gray-900"
placeholder="Enter description"
placeholderTextColor="#9ca3af" placeholderTextColor="#9ca3af"
value={description} value={description}
onChangeText={setDescription} onChangeText={setDescription}
@@ -152,10 +169,12 @@ export default function EditTask() {
/> />
</View> </View>
<View style={styles.field}> <View className="mb-4">
<Text style={styles.label}>Deadline</Text> <Text className="mb-2 text-sm font-semibold text-gray-700">
Deadline
</Text>
<TextInput <TextInput
style={styles.input} className="rounded-xl border border-gray-300 bg-gray-50 px-4 py-3 text-base text-gray-900"
placeholder="YYYY-MM-DD" placeholder="YYYY-MM-DD"
placeholderTextColor="#9ca3af" placeholderTextColor="#9ca3af"
value={deadline} value={deadline}
@@ -164,52 +183,57 @@ export default function EditTask() {
</View> </View>
<Pressable <Pressable
style={[ className={`mb-6 flex-row items-center rounded-xl border p-4 ${
styles.checkboxContainer, isCompleted
isCompleted && styles.checkboxContainerActive, ? 'border-blue-600 bg-blue-50'
]} : 'border-gray-300 bg-gray-50'
}`}
onPress={() => setIsCompleted((current) => !current)} onPress={() => setIsCompleted((current) => !current)}
> >
<View <View
style={[ className={`mr-3 h-6 w-6 items-center justify-center rounded-md border-2 ${
styles.checkbox, isCompleted
isCompleted && styles.checkboxActive, ? 'border-blue-600 bg-blue-600'
]} : 'border-gray-400 bg-white'
}`}
> >
{isCompleted && <Text style={styles.checkboxMark}></Text>} {isCompleted && (
<Text className="text-base font-bold text-white"></Text>
)}
</View> </View>
<Text style={styles.checkboxLabel}> <Text className="text-base font-semibold text-gray-900">
{isCompleted ? 'Completed' : 'Not completed'} {isCompleted ? 'Completed' : 'Not completed'}
</Text> </Text>
</Pressable> </Pressable>
<View style={styles.buttonGroup}> <Pressable
<Pressable className={`h-14 items-center justify-center rounded-2xl ${
style={[ isSaving ? 'bg-blue-400' : 'bg-blue-600'
styles.primaryButton, }`}
isSaving && styles.disabledButton, onPress={handleSaveTask}
]} disabled={isSaving}
onPress={handleSaveTask} >
disabled={isSaving} <Text className="text-base font-bold text-white">
> {isSaving ? 'Saving...' : 'Save Changes'}
<Text style={styles.primaryButtonText}> </Text>
{isSaving ? 'Saving...' : 'Save Changes'} </Pressable>
</Text>
</Pressable>
{isSaving && ( {isSaving && (
<ActivityIndicator size="small" /> <View className="mt-4">
<ActivityIndicator size="small" />
</View>
)} )}
<Pressable <Pressable
style={styles.secondaryButton} className="mt-3 h-14 items-center justify-center rounded-2xl bg-gray-200"
onPress={() => router.back()} onPress={() => router.back()}
disabled={isSaving} disabled={isSaving}
> >
<Text style={styles.secondaryButtonText}>Cancel</Text> <Text className="text-base font-bold text-gray-900">
</Pressable> Cancel
</View> </Text>
</Pressable>
</> </>
)} )}
</View> </View>
@@ -218,135 +242,4 @@ export default function EditTask() {
</KeyboardAvoidingView> </KeyboardAvoidingView>
</> </>
); );
} }
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: 'f3f4f6'
},
container: {
flexGrow: 1,
justifyContent: 'center',
padding: 20,
},
card: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
shadowRadius: 10,
elevation: 5,
},
subtitle: {
fontSize: 15,
color: '#6b7280',
marginBottom: 24,
},
loadingContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
gap: 12,
},
loadingText: {
color: '#6b7280',
fontSize: 15,
},
field: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
marginBottom: 8,
},
input: {
minHeight: 50,
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 16,
color: '#111827',
backgroundColor: '#f9fafb',
},
textArea: {
minHeight: 110,
},
checkboxContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 12,
padding: 14,
marginTop: 4,
marginBottom: 22,
backgroundColor: '#f9fafb',
},
checkboxContainerActive: {
borderColor: '#2563eb',
backgroundColor: '#eff6ff',
},
checkbox: {
width: 24,
height: 24,
borderWidth: 2,
borderColor: '#9ca3af',
borderRadius: 6,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff',
},
checkboxActive: {
borderColor: '#2563eb',
backgroundColor: '#2563eb',
},
checkboxMark: {
color: '#ffffff',
fontSize: 16,
fontWeight: '700',
},
checkboxLabel: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
},
buttonGroup: {
gap: 12,
},
primaryButton: {
height: 52,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#2563eb',
},
primaryButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '700',
},
disabledButton: {
opacity: 0.6,
},
secondaryButton: {
height: 52,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#e5e7eb',
},
secondaryButtonText: {
color: '#111827',
fontSize: 16,
fontWeight: '700',
},
});

180
app/(tabs)/timer.tsx Normal file
View File

@@ -0,0 +1,180 @@
import * as React from 'react';
import {
Animated,
Dimensions,
StatusBar,
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
const { width, height } = Dimensions.get('window');
const colors = {
black: '#323F4E',
red: '#F76A6A',
text: '#ffffff',
};
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;
/*
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
*/
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([
Animated.timing(buttonAnimation, {
toValue: 1,
duration: 300,
useNativeDriver: true
}),
Animated.timing(timerAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true
}),
Animated.timing(timerAnimation, {
toValue: height,
duration: duration * 1000,
useNativeDriver: true
}),
]) .start(() => {
Animated.timing(buttonAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true
}).start()
})
}, [duration])
const opacity = buttonAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 0]
})
const translateY = buttonAnimation.interpolate({
inputRange: [0, 1],
outputRange: [0, 200]
})
return (
<View style={styles.container}>
<StatusBar hidden />
<Animated.View
style={[StyleSheet.absoluteFillObject, {
height,
width,
backgroundColor: colors.red,
transform: [{
translateY: timerAnimation
}]
}]}
/>
<Animated.View
style={[
StyleSheet.absoluteFillObject,
{
justifyContent: 'flex-end',
alignItems: 'center',
paddingBottom: 100,
opacity,
transform: [{
translateY
}]
},
]}>
<TouchableOpacity
onPress={animation}>
<View
style={styles.roundButton}
/>
</TouchableOpacity>
</Animated.View>
<View
style={{
position: 'absolute',
top: height / 3,
left: 0,
right: 0,
flex: 1,
}}>
<Animated.FlatList
data={timers}
keyExtractor={item => item.toString()}
horizontal
bounces={false}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}],
{ useNativeDriver: true}
)}
showsHorizontalScrollIndicator={false}
onMomentumScrollEnd={ev => {
const index = Math.round(ev.nativeEvent.contentOffset.x / ITEM_SIZE)
setDuration(timers[index]);
}}
snapToInterval={ITEM_SIZE}
decelerationRate={"fast"}
style={{flexGrow: 0}}
contentContainerStyle={{
paddingHorizontal: ITEM_SPACING
}}
renderItem={({item, index}) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
]
const opacity = scrollX.interpolate({
inputRange,
outputRange: [.4, 1, .4]
})
const scale = scrollX.interpolate({
inputRange,
outputRange: [.7, 1, .7]
})
return <View style={{width: ITEM_SIZE, justifyContent: 'center', alignItems: 'center'}}>
<Animated.Text style={[styles.text, {
opacity,
transform: [{
scale
}]
}]}>
{item}
</Animated.Text>
</View>
}
}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.black,
},
roundButton: {
width: 80,
height: 80,
borderRadius: 80,
backgroundColor: colors.red,
},
text: {
fontSize: ITEM_SIZE * 0.8,
fontFamily: 'Menlo',
color: colors.text,
fontWeight: '900',
}
});

View File

@@ -1,4 +1,5 @@
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import '../global.css';
export default function RootLayout() { export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />; return <Stack screenOptions={{ headerShown: false }} />;

10
babel.config.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
],
};
};

3
global.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

8
metro.config.js Normal file
View File

@@ -0,0 +1,8 @@
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, {
input: './global.css',
});

3
nativewind-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

852
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,12 +23,13 @@
"expo-image": "~3.0.11", "expo-image": "~3.0.11",
"expo-linking": "~8.0.11", "expo-linking": "~8.0.11",
"expo-router": "~6.0.23", "expo-router": "~6.0.23",
"expo-secure-store": "^55.0.13", "expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13", "expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8", "expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9", "expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.10", "expo-web-browser": "~15.0.10",
"nativewind": "^4.2.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-native": "0.81.5", "react-native": "0.81.5",
@@ -38,7 +39,8 @@
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-url-polyfill": "^3.0.0", "react-native-url-polyfill": "^3.0.0",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1" "react-native-worklets": "0.5.1",
"tailwindcss": "^3.4.19"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",

13
tailwind.config.js Normal file
View File

@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
'./constants/**/*.{js,jsx,ts,tsx}',
],
presets: [require('nativewind/preset')],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -12,6 +12,7 @@
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
".expo/types/**/*.ts", ".expo/types/**/*.ts",
"expo-env.d.ts" "expo-env.d.ts",
"nativewind-env.d.ts"
] ]
} }