ui update and some backend functionality adding in accordance with research on adhd and ux design
This commit is contained in:
@@ -20,6 +20,7 @@ async function request<T>(
|
||||
const token = getToken();
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Timezone-Offset': String(new Date().getTimezoneOffset()),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
};
|
||||
@@ -375,6 +376,14 @@ export const api = {
|
||||
duration_minutes?: number;
|
||||
position: number;
|
||||
} | null;
|
||||
celebration?: {
|
||||
streak_current: number;
|
||||
streak_longest: number;
|
||||
session_duration_minutes: number;
|
||||
total_completions: number;
|
||||
steps_completed: number;
|
||||
steps_skipped: number;
|
||||
};
|
||||
}>(`/api/sessions/${sessionId}/complete-step`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ step_id: stepId }),
|
||||
@@ -392,6 +401,14 @@ export const api = {
|
||||
duration_minutes?: number;
|
||||
position: number;
|
||||
} | null;
|
||||
celebration?: {
|
||||
streak_current: number;
|
||||
streak_longest: number;
|
||||
session_duration_minutes: number;
|
||||
total_completions: number;
|
||||
steps_completed: number;
|
||||
steps_skipped: number;
|
||||
};
|
||||
}>(`/api/sessions/${sessionId}/skip-step`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ step_id: stepId }),
|
||||
@@ -546,6 +563,68 @@ export const api = {
|
||||
},
|
||||
},
|
||||
|
||||
// Victories
|
||||
victories: {
|
||||
get: async (days = 30) => {
|
||||
return request<Array<{
|
||||
type: string;
|
||||
message: string;
|
||||
date?: string;
|
||||
}>>(`/api/victories?days=${days}`, { method: 'GET' });
|
||||
},
|
||||
},
|
||||
|
||||
// Rewards
|
||||
rewards: {
|
||||
getRandom: async (context = 'completion') => {
|
||||
return request<{
|
||||
reward: {
|
||||
id: string;
|
||||
category: string;
|
||||
content: string;
|
||||
emoji?: string;
|
||||
rarity: string;
|
||||
} | null;
|
||||
}>(`/api/rewards/random?context=${context}`, { method: 'GET' });
|
||||
},
|
||||
|
||||
getHistory: async () => {
|
||||
return request<Array<{
|
||||
earned_at: string;
|
||||
context?: string;
|
||||
category: string;
|
||||
content: string;
|
||||
emoji?: string;
|
||||
rarity?: string;
|
||||
}>>('/api/rewards/history', { method: 'GET' });
|
||||
},
|
||||
},
|
||||
|
||||
// Preferences
|
||||
preferences: {
|
||||
get: async () => {
|
||||
return request<{
|
||||
sound_enabled: boolean;
|
||||
haptic_enabled: boolean;
|
||||
show_launch_screen: boolean;
|
||||
celebration_style: string;
|
||||
}>('/api/preferences', { method: 'GET' });
|
||||
},
|
||||
|
||||
update: async (data: {
|
||||
sound_enabled?: boolean;
|
||||
haptic_enabled?: boolean;
|
||||
show_launch_screen?: boolean;
|
||||
celebration_style?: string;
|
||||
timezone_offset?: number;
|
||||
}) => {
|
||||
return request<Record<string, unknown>>('/api/preferences', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
// Notifications
|
||||
notifications: {
|
||||
getVapidPublicKey: async () => {
|
||||
|
||||
19
synculous-client/src/lib/haptics.ts
Normal file
19
synculous-client/src/lib/haptics.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
function vibrate(pattern: number | number[]): void {
|
||||
if (typeof navigator !== 'undefined' && navigator.vibrate) {
|
||||
navigator.vibrate(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
export function hapticTap() {
|
||||
vibrate(10);
|
||||
}
|
||||
|
||||
export function hapticSuccess() {
|
||||
vibrate([10, 50, 10]);
|
||||
}
|
||||
|
||||
export function hapticCelebration() {
|
||||
vibrate([10, 30, 10, 30, 50]);
|
||||
}
|
||||
48
synculous-client/src/lib/sounds.ts
Normal file
48
synculous-client/src/lib/sounds.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
let audioContext: AudioContext | null = null;
|
||||
|
||||
function getAudioContext(): AudioContext {
|
||||
if (!audioContext) {
|
||||
audioContext = new AudioContext();
|
||||
}
|
||||
return audioContext;
|
||||
}
|
||||
|
||||
function playTone(frequency: number, duration: number, type: OscillatorType = 'sine', volume = 0.15) {
|
||||
try {
|
||||
const ctx = getAudioContext();
|
||||
const oscillator = ctx.createOscillator();
|
||||
const gainNode = ctx.createGain();
|
||||
|
||||
oscillator.type = type;
|
||||
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
|
||||
gainNode.gain.setValueAtTime(volume, ctx.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(ctx.destination);
|
||||
|
||||
oscillator.start(ctx.currentTime);
|
||||
oscillator.stop(ctx.currentTime + duration);
|
||||
} catch {
|
||||
// Audio not available
|
||||
}
|
||||
}
|
||||
|
||||
export function playStepComplete() {
|
||||
playTone(523, 0.1, 'sine', 0.12);
|
||||
setTimeout(() => playTone(659, 0.15, 'sine', 0.12), 80);
|
||||
}
|
||||
|
||||
export function playCelebration() {
|
||||
const notes = [523, 659, 784, 1047];
|
||||
notes.forEach((freq, i) => {
|
||||
setTimeout(() => playTone(freq, 0.2, 'sine', 0.1), i * 120);
|
||||
});
|
||||
}
|
||||
|
||||
export function playTimerEnd() {
|
||||
playTone(880, 0.15, 'triangle', 0.1);
|
||||
setTimeout(() => playTone(880, 0.15, 'triangle', 0.1), 200);
|
||||
}
|
||||
Reference in New Issue
Block a user