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

314 lines
8.3 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 server';
import { createServerClient } from '@/utils/supabase';
import type { Profile, Result } from '@/utils/supabase';
import { getUser, getProfileWithAvatar, getSignedUrl } from '@/lib/actions';
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 = await createServerClient();
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 = await createServerClient();
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<void>> => {
try {
const supabase = await createServerClient();
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("Couldn't insert statuses!");
else if (insertedStatuses) {
const createdAtFallback = new Date(Date.now()).toISOString();
await broadcastStatusUpdates(
usersWithStatuses.map((s, i) => {
return {
user: s.user,
status: status,
created_at: insertedStatuses[i]?.created_at ?? createdAtFallback,
updated_by: user,
};
}),
);
}
return { success: true, data: undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
};
export const updateUserStatus = async (
status: string,
): Promise<Result<void>> => {
try {
const supabase = await createServerClient();
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;
await broadcastStatusUpdates([
{
user: userProfile,
status: insertedStatus.status,
created_at: insertedStatus.created_at,
updated_by: userProfile,
},
]);
return { success: true, data: undefined };
} 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 = await createServerClient();
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 = await createServerClient();
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}`,
};
}
};