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
- Node.js
- Node.js 20.19.4 or newer
- npm
- Expo CLI
- 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 { useEffect, useState } from "react";
export default function TabLayout() {
const [session, SetSession] = useState<Session | null>(null)
const [loading, SetLoading] = useState(true);

View File

@@ -1,4 +1,3 @@
import { defaultStyles } from '@/constants/defaultStyles';
import { supabase } from '@/lib/supabase';
import {
router,
@@ -15,11 +14,10 @@ import {
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableWithoutFeedback,
View
View,
} from 'react-native';
export default function EditTask() {
@@ -66,84 +64,103 @@ export default function EditTask() {
}, [taskId])
);
const handleSaveTask = async () => {
const handleSaveTask = async () => {
if (!title.trim() || !description.trim() || !deadline.trim()) {
Alert.alert("All fields are required!");
Alert.alert('All fields are required!');
return;
}
setIsSaving(true);
try {
const { data: userData, error: userError } = await supabase.auth.getUser();
if (userError || !userData.user) {
router.replace("../createUser");
router.replace('../createUser');
return;
}
const { error: dbError } = await supabase.from("tasks").update({
title,
description,
isCompleted,
lastChanged: new Date().toISOString(),
deadline,
uId: userData.user.id,
}).eq("tId", taskId);
const { error: dbError } = await supabase
.from('tasks')
.update({
title: title.trim(),
description: description.trim(),
isCompleted,
lastChanged: new Date().toISOString(),
deadline: deadline.trim(),
uId: userData.user.id,
})
.eq('tId', taskId);
if (dbError) {
Alert.alert("Task could not be edited, please try again");
Alert.alert('Task could not be edited, please try again');
return;
}
Alert.alert("Task successfully edited!");
Alert.alert('Task successfully edited!');
router.back();
} finally {
setIsSaving(false);
}
}
};
return (
<>
<Stack.Screen
options={{
title: "Edit Task",
headerTitleStyle: defaultStyles.title
title: 'Edit Task',
headerTitleStyle: {
fontSize: 20,
fontWeight: '700',
},
}}
/>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
<KeyboardAvoidingView
className="flex-1 bg-gray-100"
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<ScrollView
style={defaultStyles.container}
keyboardShouldPersistTaps="handled"
<ScrollView
keyboardShouldPersistTaps="handled"
contentContainerClassName="flex-grow justify-center px-5 py-8"
>
<View style={styles.card}>
<Text style={defaultStyles.title}>Edit Task</Text>
<Text style={styles.subtitle}>Update your task details below.</Text>
<View className="rounded-3xl bg-white p-6 shadow-lg">
<Text className="mb-1 text-3xl font-bold text-gray-900">
Edit Task
</Text>
<Text className="mb-6 text-base text-gray-500">
Update the details for this task.
</Text>
{isLoading ? (
<View style={styles.loadingContainer}>
<View className="items-center justify-center py-12">
<ActivityIndicator size="large" />
<Text style={styles.loadingText}>Loading task...</Text>
<Text className="mt-3 text-gray-500">Loading task...</Text>
</View>
) : (
<>
<View style={styles.field}>
<Text style={styles.label}>Title</Text>
<TextInput
style={styles.input}
placeholder="Enter task title"
value={title}
onChangeText={setTitle}
/>
</View>
<View style={styles.field}>
<Text style={styles.label}>Description</Text>
<View className="mb-4">
<Text className="mb-2 text-sm font-semibold text-gray-700">
Title
</Text>
<TextInput
style={[styles.input, styles.textArea]}
placeholder="Enter task description"
className="rounded-xl border border-gray-300 bg-gray-50 px-4 py-3 text-base text-gray-900"
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"
value={description}
onChangeText={setDescription}
@@ -152,10 +169,12 @@ export default function EditTask() {
/>
</View>
<View style={styles.field}>
<Text style={styles.label}>Deadline</Text>
<View className="mb-4">
<Text className="mb-2 text-sm font-semibold text-gray-700">
Deadline
</Text>
<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"
placeholderTextColor="#9ca3af"
value={deadline}
@@ -164,52 +183,57 @@ export default function EditTask() {
</View>
<Pressable
style={[
styles.checkboxContainer,
isCompleted && styles.checkboxContainerActive,
]}
className={`mb-6 flex-row items-center rounded-xl border p-4 ${
isCompleted
? 'border-blue-600 bg-blue-50'
: 'border-gray-300 bg-gray-50'
}`}
onPress={() => setIsCompleted((current) => !current)}
>
<View
style={[
styles.checkbox,
isCompleted && styles.checkboxActive,
]}
<View
className={`mr-3 h-6 w-6 items-center justify-center rounded-md border-2 ${
isCompleted
? '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>
<Text style={styles.checkboxLabel}>
<Text className="text-base font-semibold text-gray-900">
{isCompleted ? 'Completed' : 'Not completed'}
</Text>
</Pressable>
<View style={styles.buttonGroup}>
<Pressable
style={[
styles.primaryButton,
isSaving && styles.disabledButton,
]}
onPress={handleSaveTask}
disabled={isSaving}
>
<Text style={styles.primaryButtonText}>
{isSaving ? 'Saving...' : 'Save Changes'}
</Text>
</Pressable>
<Pressable
className={`h-14 items-center justify-center rounded-2xl ${
isSaving ? 'bg-blue-400' : 'bg-blue-600'
}`}
onPress={handleSaveTask}
disabled={isSaving}
>
<Text className="text-base font-bold text-white">
{isSaving ? 'Saving...' : 'Save Changes'}
</Text>
</Pressable>
{isSaving && (
<ActivityIndicator size="small" />
<View className="mt-4">
<ActivityIndicator size="small" />
</View>
)}
<Pressable
style={styles.secondaryButton}
onPress={() => router.back()}
disabled={isSaving}
>
<Text style={styles.secondaryButtonText}>Cancel</Text>
</Pressable>
</View>
<Pressable
className="mt-3 h-14 items-center justify-center rounded-2xl bg-gray-200"
onPress={() => router.back()}
disabled={isSaving}
>
<Text className="text-base font-bold text-gray-900">
Cancel
</Text>
</Pressable>
</>
)}
</View>
@@ -218,135 +242,4 @@ export default function EditTask() {
</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 '../global.css';
export default function RootLayout() {
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-linking": "~8.0.11",
"expo-router": "~6.0.23",
"expo-secure-store": "^55.0.13",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.10",
"nativewind": "^4.2.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
@@ -38,7 +39,8 @@
"react-native-screens": "~4.16.0",
"react-native-url-polyfill": "^3.0.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
"react-native-worklets": "0.5.1",
"tailwindcss": "^3.4.19"
},
"devDependencies": {
"@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",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}
}