Files
tech-tracker-next/src/lib/hooks/status.ts

314 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { createClient } from '@/utils/supabase';
import type { Profile, Result } from '@/utils/supabase';
import { getUser, getProfileWithAvatar, getSignedUrl } from '@/lib/hooks';
export type UserWithStatus = {
id?: string;
user: Profile;
status: string;
created_at: string;
updated_by?: Profile;
};
type PaginatedHistory = {
statuses: UserWithStatus[];
meta: {
current_page: number;
per_page: number;
total_pages: number;
total_count: number;
};
};
export const getRecentUsersWithStatuses = async (): Promise<
Result<UserWithStatus[]>
> => {
const getAvatarUrl = async (url: string | null | undefined) => {
if (!url) return null;
const avatarUrl = await getSignedUrl({
bucket: 'avatars',
url,
transform: { width: 128, height: 128 },
});
if (avatarUrl.success) return avatarUrl.data;
else return null;
};
try {
const supabase = createClient();
const oneDayAgo = new Date(Date.now() - 1000 * 60 * 60 * 24);
const { data, error } = (await supabase
.from('statuses')
.select(
`
user:profiles!user_id(*),
status,
created_at,
updated_by:profiles!updated_by_id(*)
`,
)
.gte('created_at', oneDayAgo.toISOString())
.order('created_at', { ascending: false })) as {
data: UserWithStatus[];
error: unknown;
};
if (error) throw error as Error;
if (!data?.length) return { success: true, data: [] };
// 3⃣ client-side dedupe: keep the first status you see per user
const seen = new Set<string>();
const filtered = data.filter((row) => {
if (seen.has(row.user.id)) return false;
seen.add(row.user.id);
return true;
});
const filteredWithAvatars = new Array<UserWithStatus>();
for (const userWithStatus of filtered) {
if (userWithStatus.user.avatar_url)
userWithStatus.user.avatar_url = await getAvatarUrl(
userWithStatus.user.avatar_url,
);
if (userWithStatus.updated_by?.avatar_url)
userWithStatus.updated_by.avatar_url = await getAvatarUrl(
userWithStatus.updated_by?.avatar_url,
);
filteredWithAvatars.push(userWithStatus);
}
return { success: true, data: filteredWithAvatars };
} catch (error) {
return { success: false, error: `Error: ${error as Error}` };
}
};
export const broadcastStatusUpdates = async (
userStatuses: UserWithStatus[],
): Promise<Result<void>> => {
try {
const supabase = createClient();
for (const userStatus of userStatuses) {
const broadcast = await supabase.channel('status_updates').send({
type: 'broadcast',
event: 'status_updated',
payload: {
user_status: userStatus,
timestamp: new Date().toISOString(),
},
});
if (broadcast === 'error' || broadcast === 'timed out')
throw new Error(
'Failed to broadcast status update. Timed out or errored.',
);
}
return { success: true, data: undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
export const updateStatuses = async (
usersWithStatuses: UserWithStatus[],
status: string,
): Promise<Result<UserWithStatus[]>> => {
try {
const supabase = createClient();
const profileResponse = await getProfileWithAvatar();
if (!profileResponse.success) throw new Error('Not authenticated!');
const user = profileResponse.data;
const { data: insertedStatuses, error: insertedStatusesError } =
await supabase
.from('statuses')
.insert(
usersWithStatuses.map((userWithStatus) => ({
user_id: userWithStatus.user.id,
status,
updated_by_id: user.id,
})),
)
.select();
if (insertedStatusesError) throw new Error("Error inserting statuses!");
else if (insertedStatuses) {
const createdAtFallback = new Date(Date.now()).toISOString();
const statusUpdates = usersWithStatuses.map((s, i) => {
return {
user: s.user,
status: status,
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
updated_by: user,
}
});
await broadcastStatusUpdates(statusUpdates);
return { success: true, data: statusUpdates };
} else {
return { success: false, error: 'No inserted statuses returned!' };
}
} catch (error) {
return {
success: false,
error: `Error updating statuses: ${error as Error}`,
};
}
};
export const updateUserStatus = async (
status: string,
): Promise<Result<UserWithStatus[]>> => {
try {
const supabase = createClient();
const profileResponse = await getProfileWithAvatar();
if (!profileResponse.success)
throw new Error(`Not authenticated! ${profileResponse.error}`);
const userProfile = profileResponse.data;
const { data: insertedStatus, error: insertedStatusError } = await supabase
.from('statuses')
.insert({
user_id: userProfile.id,
status,
updated_by_id: userProfile.id,
})
.select()
.single();
if (insertedStatusError) throw insertedStatusError as Error;
const statusUpdate = {
user: userProfile,
status: insertedStatus.status,
created_at: insertedStatus.created_at,
updated_by: userProfile,
};
await broadcastStatusUpdates([statusUpdate]);
return { success: true, data: [statusUpdate] };
} catch (error) {
return {
success: false,
error: `Error updating user's status: ${error as Error}`,
};
}
};
export const getUserHistory = async (
userId: string,
page = 1,
perPage = 50,
): Promise<Result<PaginatedHistory>> => {
try {
const supabase = createClient();
const userResponse = await getUser();
if (!userResponse.success)
throw new Error(`Not authenticated! ${userResponse.error}`);
const offset = (page - 1) * perPage;
const { count } = await supabase
.from('statuses')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId);
const { data: statuses, error: statusesError } = (await supabase
.from('statuses')
.select(
`
id,
user:profiles!user_id(*),
status,
created_at,
updated_by:profiles!updated_by_id(*)
`,
)
.eq('user_id', userId)
.order('created_at', { ascending: false })
.range(offset, offset + perPage - 1)) as {
data: UserWithStatus[];
error: unknown;
};
if (statusesError) throw statusesError as Error;
const totalCount = count ?? 0;
const totalPages = Math.ceil(totalCount / perPage);
return {
success: true,
data: {
statuses,
meta: {
current_page: page,
per_page: perPage,
total_pages: totalPages,
total_count: totalCount,
},
},
};
} catch (error) {
return {
success: false,
error: `Error getting user's history: ${error as Error}`,
};
}
};
export const getAllHistory = async (
page = 1,
perPage = 50,
): Promise<Result<PaginatedHistory>> => {
try {
const supabase = createClient();
const userResponse = await getUser();
if (!userResponse.success)
throw new Error(`Not authenticated! ${userResponse.error}`);
const offset = (page - 1) * perPage;
const { count } = await supabase
.from('statuses')
.select('*', { count: 'exact', head: true });
const { data: statuses, error: statusesError } = (await supabase
.from('statuses')
.select(
`
id,
user:profiles!user_id(*),
status,
created_at,
updated_by:profiles!updated_by_id(*)
`,
)
.order('created_at', { ascending: false })
.range(offset, offset + perPage - 1)) as {
data: UserWithStatus[];
error: unknown;
};
if (statusesError) throw statusesError as Error;
const totalCount = count ?? 0;
const totalPages = Math.ceil(totalCount / perPage);
return {
success: true,
data: {
statuses,
meta: {
current_page: page,
per_page: perPage,
total_pages: totalPages,
total_count: totalCount,
},
},
};
} catch (error) {
return {
success: false,
error: `Error getting all history: ${error as Error}`,
};
}
};