314 lines
8.4 KiB
TypeScript
314 lines
8.4 KiB
TypeScript
'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}`,
|
||
};
|
||
}
|
||
};
|