React Native Expo app — full mobile client
- Expo SDK 54, expo-router, NativeWind - 7 screens: login, tasks, calendar, goals, chat, settings, tabs - API client (api.hasdo.info), SecureStore auth, AuthContext - Tab navigation: Ukoly, Kalendar, Cile, Chat, Nastaveni - Pull-to-refresh, FAB, group colors, priority dots - Calendar grid with task dots - AI chat with keyboard avoiding - Web export verified (780 modules, 1.5MB bundle) - Bundle ID: info.hasdo.taskteam Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
mobile/lib/AuthContext.tsx
Normal file
17
mobile/lib/AuthContext.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useAuth } from './useAuth';
|
||||
|
||||
type AuthContextType = ReturnType<typeof useAuth>;
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const auth = useAuth();
|
||||
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuthContext(): AuthContextType {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuthContext must be used within AuthProvider');
|
||||
return ctx;
|
||||
}
|
||||
64
mobile/lib/api.ts
Normal file
64
mobile/lib/api.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
const API_BASE = 'https://api.hasdo.info';
|
||||
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
opts: { method?: string; body?: any; token?: string } = {}
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (opts.token) headers['Authorization'] = `Bearer ${opts.token}`;
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: opts.method || 'GET',
|
||||
headers,
|
||||
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ message: `HTTP ${res.status}` }));
|
||||
throw new Error(err.message || `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Auth
|
||||
export const login = (data: { email: string; password: string }) =>
|
||||
apiFetch<{ data: { token: string; user: any } }>('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
|
||||
export const register = (data: { email: string; name: string; password: string }) =>
|
||||
apiFetch<{ data: { token: string; user: any } }>('/api/v1/auth/register', {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
});
|
||||
|
||||
// Tasks
|
||||
export const getTasks = (token: string) =>
|
||||
apiFetch<{ data: any[] }>('/api/v1/tasks', { token });
|
||||
|
||||
export const createTask = (token: string, data: any) =>
|
||||
apiFetch<{ data: any }>('/api/v1/tasks', { method: 'POST', body: data, token });
|
||||
|
||||
export const updateTask = (token: string, id: string, data: any) =>
|
||||
apiFetch<{ data: any }>(`/api/v1/tasks/${id}`, { method: 'PUT', body: data, token });
|
||||
|
||||
export const deleteTask = (token: string, id: string) =>
|
||||
apiFetch<void>(`/api/v1/tasks/${id}`, { method: 'DELETE', token });
|
||||
|
||||
// Groups
|
||||
export const getGroups = (token: string) =>
|
||||
apiFetch<{ data: any[] }>('/api/v1/groups', { token });
|
||||
|
||||
// Goals
|
||||
export const getGoals = (token: string) =>
|
||||
apiFetch<{ data: any[] }>('/api/v1/goals', { token });
|
||||
|
||||
// Chat
|
||||
export const sendChatMessage = (token: string, message: string) =>
|
||||
apiFetch<{ data: { reply: string } }>('/api/v1/chat', {
|
||||
method: 'POST',
|
||||
body: { message },
|
||||
token,
|
||||
});
|
||||
60
mobile/lib/auth.ts
Normal file
60
mobile/lib/auth.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const TOKEN_KEY = 'taskteam_token';
|
||||
const USER_KEY = 'taskteam_user';
|
||||
|
||||
export async function getToken(): Promise<string | null> {
|
||||
if (Platform.OS === 'web') {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
return SecureStore.getItemAsync(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export async function setToken(token: string): Promise<void> {
|
||||
if (Platform.OS === 'web') {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
return;
|
||||
}
|
||||
await SecureStore.setItemAsync(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export async function removeToken(): Promise<void> {
|
||||
if (Platform.OS === 'web') {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
return;
|
||||
}
|
||||
await SecureStore.deleteItemAsync(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export async function getUser(): Promise<any | null> {
|
||||
let raw: string | null;
|
||||
if (Platform.OS === 'web') {
|
||||
raw = localStorage.getItem(USER_KEY);
|
||||
} else {
|
||||
raw = await SecureStore.getItemAsync(USER_KEY);
|
||||
}
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUser(user: any): Promise<void> {
|
||||
const raw = JSON.stringify(user);
|
||||
if (Platform.OS === 'web') {
|
||||
localStorage.setItem(USER_KEY, raw);
|
||||
return;
|
||||
}
|
||||
await SecureStore.setItemAsync(USER_KEY, raw);
|
||||
}
|
||||
|
||||
export async function removeUser(): Promise<void> {
|
||||
if (Platform.OS === 'web') {
|
||||
localStorage.removeItem(USER_KEY);
|
||||
return;
|
||||
}
|
||||
await SecureStore.deleteItemAsync(USER_KEY);
|
||||
}
|
||||
44
mobile/lib/useAuth.ts
Normal file
44
mobile/lib/useAuth.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getToken, setToken, removeToken, getUser, setUser, removeUser } from './auth';
|
||||
import * as api from './api';
|
||||
|
||||
export function useAuth() {
|
||||
const [token, setTokenState] = useState<string | null>(null);
|
||||
const [user, setUserState] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const t = await getToken();
|
||||
const u = await getUser();
|
||||
setTokenState(t);
|
||||
setUserState(u);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const loginFn = useCallback(async (email: string, password: string) => {
|
||||
const res = await api.login({ email, password });
|
||||
await setToken(res.data.token);
|
||||
await setUser(res.data.user);
|
||||
setTokenState(res.data.token);
|
||||
setUserState(res.data.user);
|
||||
}, []);
|
||||
|
||||
const registerFn = useCallback(async (email: string, name: string, password: string) => {
|
||||
const res = await api.register({ email, name, password });
|
||||
await setToken(res.data.token);
|
||||
await setUser(res.data.user);
|
||||
setTokenState(res.data.token);
|
||||
setUserState(res.data.user);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await removeToken();
|
||||
await removeUser();
|
||||
setTokenState(null);
|
||||
setUserState(null);
|
||||
}, []);
|
||||
|
||||
return { token, user, loading, login: loginFn, register: registerFn, logout };
|
||||
}
|
||||
Reference in New Issue
Block a user