significant refactor
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
type LogLevel = 'info' | 'success' | 'warn' | 'error';
|
||||
type LogLevel = "info" | "success" | "warn" | "error";
|
||||
|
||||
interface LoggerOptions {
|
||||
enabled?: boolean;
|
||||
@@ -7,50 +7,38 @@ interface LoggerOptions {
|
||||
colors?: boolean;
|
||||
}
|
||||
|
||||
// Cache for performance - update once per second max
|
||||
let cachedTimestamp = '';
|
||||
let cachedTimestamp = "";
|
||||
let lastTimestampUpdate = 0;
|
||||
|
||||
/**
|
||||
* Get formatted timestamp with caching for performance
|
||||
* Format: MM/DD HH:mm:ss
|
||||
*/
|
||||
function getTimestamp(): string {
|
||||
const now = Date.now();
|
||||
|
||||
// Update cache only if more than 1 second has passed
|
||||
|
||||
if (now - lastTimestampUpdate > 1000) {
|
||||
const date = new Date(now);
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(date.getSeconds()).padStart(2, "0");
|
||||
|
||||
cachedTimestamp = `${month}/${day} ${hours}:${minutes}:${seconds}`;
|
||||
lastTimestampUpdate = now;
|
||||
}
|
||||
|
||||
|
||||
return cachedTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color and emoji for each log level
|
||||
*/
|
||||
function getLevelStyle(level: LogLevel): { color: string; label: string } {
|
||||
const styles = {
|
||||
info: { color: '#f5f5f5', label: 'INFO' },
|
||||
success: { color: '#10B981', label: 'SUCCESS' },
|
||||
warn: { color: '#F59E0B', label: 'WARN' },
|
||||
error: { color: '#EF4444', label: 'ERROR' },
|
||||
info: { color: "#f5f5f5", label: "INFO" },
|
||||
success: { color: "#10B981", label: "SUCCESS" },
|
||||
warn: { color: "#F59E0B", label: "WARN" },
|
||||
error: { color: "#EF4444", label: "ERROR" },
|
||||
};
|
||||
|
||||
|
||||
return styles[level] || styles.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main logger class
|
||||
*/
|
||||
class Logger {
|
||||
private options: LoggerOptions;
|
||||
private context?: string;
|
||||
@@ -58,25 +46,21 @@ class Logger {
|
||||
constructor(context?: string, options: LoggerOptions = {}) {
|
||||
this.context = context;
|
||||
this.options = {
|
||||
enabled: process.env.NODE_ENV !== 'production',
|
||||
enabled: process.env.NODE_ENV !== "production",
|
||||
showTimestamp: true,
|
||||
collapsed: true,
|
||||
colors: true,
|
||||
...options
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a child logger with a specific context
|
||||
*/
|
||||
child(context: string, options?: LoggerOptions): Logger {
|
||||
const childContext = this.context ? `${this.context} > ${context}` : context;
|
||||
const childContext = this.context
|
||||
? `${this.context} > ${context}`
|
||||
: context;
|
||||
return new Logger(childContext, { ...this.options, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Core logging method
|
||||
*/
|
||||
private log(
|
||||
level: LogLevel,
|
||||
label: string,
|
||||
@@ -86,114 +70,92 @@ class Logger {
|
||||
if (!this.options.enabled) return;
|
||||
|
||||
const style = getLevelStyle(level);
|
||||
const timestamp = this.options.showTimestamp ? `${getTimestamp()} │ ` : '';
|
||||
const context = this.context ? ` │ ${this.context}` : '';
|
||||
|
||||
const timestamp = this.options.showTimestamp ? `${getTimestamp()} │ ` : "";
|
||||
const context = this.context ? ` │ ${this.context}` : "";
|
||||
|
||||
const groupLabel = `${timestamp}${style.label}${context} │ ${label}`;
|
||||
|
||||
const group = this.options.collapsed ? console.groupCollapsed : console.group;
|
||||
|
||||
if (this.options.colors && typeof window !== 'undefined') {
|
||||
group(
|
||||
`%c${groupLabel}`,
|
||||
`color: ${style.color}; font-weight: bold;`
|
||||
);
|
||||
|
||||
const group = this.options.collapsed
|
||||
? console.groupCollapsed
|
||||
: console.group;
|
||||
|
||||
if (this.options.colors && typeof window !== "undefined") {
|
||||
group(`%c${groupLabel}`, `color: ${style.color}; font-weight: bold;`);
|
||||
} else {
|
||||
group(groupLabel);
|
||||
}
|
||||
|
||||
|
||||
if (data !== undefined) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
|
||||
if (rest.length > 0) {
|
||||
for (const item of rest) {
|
||||
console.log(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* log level methods
|
||||
*/
|
||||
info(label: string, data?: any, ...rest: any[]): void {
|
||||
this.log('info', label, data, ...rest);
|
||||
this.log("info", label, data, ...rest);
|
||||
}
|
||||
|
||||
success(label: string, data?: any, ...rest: any[]): void {
|
||||
this.log('success', label, data, ...rest);
|
||||
this.log("success", label, data, ...rest);
|
||||
}
|
||||
|
||||
warn(label: string, data?: any, ...rest: any[]): void {
|
||||
this.log('warn', label, data, ...rest);
|
||||
this.log("warn", label, data, ...rest);
|
||||
}
|
||||
|
||||
error(label: string, data?: any, ...rest: any[]): void {
|
||||
this.log('error', label, data, ...rest);
|
||||
this.log("error", label, data, ...rest);
|
||||
}
|
||||
|
||||
simple(message: string): void {
|
||||
if (!this.options.enabled) return;
|
||||
|
||||
const style = getLevelStyle('info');
|
||||
const timestamp = this.options.showTimestamp ? `${getTimestamp()} │ ` : '';
|
||||
const context = this.context ? ` │ ${this.context}` : '';
|
||||
|
||||
|
||||
const style = getLevelStyle("info");
|
||||
const timestamp = this.options.showTimestamp ? `${getTimestamp()} │ ` : "";
|
||||
const context = this.context ? ` │ ${this.context}` : "";
|
||||
|
||||
const logMessage = `${timestamp}${style.label}${context} │ ${message}`;
|
||||
|
||||
if (this.options.colors && typeof window !== 'undefined') {
|
||||
|
||||
if (this.options.colors && typeof window !== "undefined") {
|
||||
console.log(`%c${logMessage}`, `color: ${style.color};`);
|
||||
} else {
|
||||
console.log(logMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure performance of an operation
|
||||
*/
|
||||
async measure<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
||||
const start = performance.now();
|
||||
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
const duration = (performance.now() - start).toFixed(2);
|
||||
|
||||
|
||||
this.success(`${label} completed`, {
|
||||
duration: `${duration}ms`,
|
||||
result
|
||||
result,
|
||||
});
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = (performance.now() - start).toFixed(2);
|
||||
|
||||
|
||||
this.error(`${label} failed`, {
|
||||
duration: `${duration}ms`,
|
||||
error
|
||||
error,
|
||||
});
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table log
|
||||
*/
|
||||
table(label: string, data: any[]): void {
|
||||
if (!this.options.enabled) return;
|
||||
|
||||
const timestamp = this.options.showTimestamp ? `${getTimestamp()} │ ` : '';
|
||||
const context = this.context ? ` │ ${this.context}` : '';
|
||||
|
||||
console.group(`${timestamp}TABLE${context} │ ${label}`);
|
||||
console.table(data);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const logger = new Logger();
|
||||
|
||||
export { Logger };
|
||||
|
||||
@@ -4,20 +4,25 @@ import { useMantineColorScheme } from "@mantine/core";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const ColorSchemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { metadata: { colorScheme } } = useAuth()
|
||||
const {
|
||||
metadata: { colorScheme },
|
||||
} = useAuth();
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!colorScheme) return;
|
||||
|
||||
|
||||
setColorScheme(colorScheme);
|
||||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
if (themeColorMeta) {
|
||||
themeColorMeta.setAttribute('content', colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0]);
|
||||
themeColorMeta.setAttribute(
|
||||
"content",
|
||||
colorScheme === "dark" ? theme.colors.dark[8] : theme.colors.gray[0]
|
||||
);
|
||||
}
|
||||
}, [colorScheme]);
|
||||
return children
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
export default ColorSchemeProvider;
|
||||
|
||||
@@ -1,68 +1,71 @@
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { createTheme, MantineProvider as MantineProviderCore } from "@mantine/core";
|
||||
import {
|
||||
createTheme,
|
||||
MantineProvider as MantineProviderCore,
|
||||
} from "@mantine/core";
|
||||
import ColorSchemeProvider from "./color-scheme-provider";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const commonInputStyles = {
|
||||
label: {
|
||||
padding: 5
|
||||
padding: 5,
|
||||
},
|
||||
root: {
|
||||
margin: '0'
|
||||
}
|
||||
}
|
||||
margin: "0",
|
||||
},
|
||||
};
|
||||
|
||||
const theme = createTheme({
|
||||
defaultRadius: 'sm',
|
||||
defaultRadius: "sm",
|
||||
components: {
|
||||
TextInput: {
|
||||
styles: commonInputStyles
|
||||
styles: commonInputStyles,
|
||||
},
|
||||
DateTimePicker: {
|
||||
styles: commonInputStyles
|
||||
styles: commonInputStyles,
|
||||
},
|
||||
Input: {
|
||||
styles: commonInputStyles
|
||||
styles: commonInputStyles,
|
||||
},
|
||||
Select: {
|
||||
styles: commonInputStyles
|
||||
styles: commonInputStyles,
|
||||
},
|
||||
Autocomplete: {
|
||||
styles: commonInputStyles
|
||||
styles: commonInputStyles,
|
||||
},
|
||||
DateTiemPicker: {
|
||||
styles: {
|
||||
root: {
|
||||
zIndex: 1000
|
||||
zIndex: 1000,
|
||||
},
|
||||
input: {
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'red'
|
||||
}
|
||||
}
|
||||
}
|
||||
backgroundColor: "red",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const MantineProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { metadata } = useAuth()
|
||||
const [isHydrated, setIsHydrated] = useState(false)
|
||||
const { metadata } = useAuth();
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsHydrated(true)
|
||||
}, [])
|
||||
setIsHydrated(true);
|
||||
}, []);
|
||||
|
||||
const colorScheme = isHydrated ? (metadata.colorScheme || 'auto') : 'auto'
|
||||
const primaryColor = isHydrated ? (metadata.accentColor || 'blue') : 'blue'
|
||||
const colorScheme = isHydrated ? metadata.colorScheme || "auto" : "auto";
|
||||
const primaryColor = isHydrated ? metadata.accentColor || "blue" : "blue";
|
||||
|
||||
return <MantineProviderCore
|
||||
defaultColorScheme={colorScheme}
|
||||
theme={{ ...theme, primaryColor }}
|
||||
>
|
||||
<ColorSchemeProvider>
|
||||
{children}
|
||||
</ColorSchemeProvider>
|
||||
</MantineProviderCore>
|
||||
}
|
||||
return (
|
||||
<MantineProviderCore
|
||||
defaultColorScheme={colorScheme}
|
||||
theme={{ ...theme, primaryColor }}
|
||||
>
|
||||
<ColorSchemeProvider>{children}</ColorSchemeProvider>
|
||||
</MantineProviderCore>
|
||||
);
|
||||
};
|
||||
|
||||
export default MantineProvider;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import { createPlayersService } from './services/players';
|
||||
import { createTournamentsService } from './services/tournaments';
|
||||
import { createTeamsService } from './services/teams';
|
||||
import PocketBase from "pocketbase";
|
||||
import { createPlayersService } from "./services/players";
|
||||
import { createTournamentsService } from "./services/tournaments";
|
||||
import { createTeamsService } from "./services/teams";
|
||||
|
||||
class PocketBaseAdminClient {
|
||||
private pb: PocketBase;
|
||||
@@ -11,20 +11,20 @@ class PocketBaseAdminClient {
|
||||
this.pb = new PocketBase(import.meta.env.VITE_POCKETBASE_URL);
|
||||
|
||||
this.pb.beforeSend = (url, options) => {
|
||||
options.cache = 'no-store';
|
||||
options.cache = "no-store";
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
};
|
||||
|
||||
|
||||
return { url, options };
|
||||
};
|
||||
this.pb.autoCancellation(false);
|
||||
|
||||
this.authPromise = this.authenticate();
|
||||
|
||||
|
||||
this.authPromise.then(() => {
|
||||
Object.assign(this, createPlayersService(this.pb));
|
||||
Object.assign(this, createTeamsService(this.pb));
|
||||
@@ -33,18 +33,20 @@ class PocketBaseAdminClient {
|
||||
}
|
||||
|
||||
private async authenticate() {
|
||||
await this.pb.collection("_superusers").authWithPassword(
|
||||
import.meta.env.VITE_POCKETBASE_ADMIN_EMAIL!,
|
||||
import.meta.env.VITE_POCKETBASE_ADMIN_PASSWORD!
|
||||
);
|
||||
await this.pb
|
||||
.collection("_superusers")
|
||||
.authWithPassword(
|
||||
import.meta.env.VITE_POCKETBASE_ADMIN_EMAIL!,
|
||||
import.meta.env.VITE_POCKETBASE_ADMIN_PASSWORD!
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface AdminClient extends
|
||||
PocketBaseAdminClient,
|
||||
ReturnType<typeof createPlayersService>,
|
||||
ReturnType<typeof createTeamsService>,
|
||||
ReturnType<typeof createTournamentsService> {
|
||||
interface AdminClient
|
||||
extends PocketBaseAdminClient,
|
||||
ReturnType<typeof createPlayersService>,
|
||||
ReturnType<typeof createTeamsService>,
|
||||
ReturnType<typeof createTournamentsService> {
|
||||
authPromise: Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
import type { Player, PlayerInput, PlayerUpdateInput } from '@/features/players/types';
|
||||
import { transformPlayer } from '@/lib/pocketbase/util/transform-types';
|
||||
import PocketBase from 'pocketbase';
|
||||
import type {
|
||||
Player,
|
||||
PlayerInput,
|
||||
PlayerUpdateInput,
|
||||
} from "@/features/players/types";
|
||||
import { transformPlayer } from "@/lib/pocketbase/util/transform-types";
|
||||
import PocketBase from "pocketbase";
|
||||
|
||||
export function createPlayersService(pb: PocketBase) {
|
||||
return {
|
||||
async getPlayerByAuthId(authId: string): Promise<Player | null> {
|
||||
const result = await pb.collection('players').getList<Player>(1, 1, {
|
||||
filter: `auth_id = "${authId}"`
|
||||
const result = await pb.collection("players").getList<Player>(1, 1, {
|
||||
filter: `auth_id = "${authId}"`,
|
||||
});
|
||||
return result.items[0] ? transformPlayer(result.items[0]) : null;
|
||||
},
|
||||
|
||||
async getPlayer(id: string): Promise<Player | null> {
|
||||
const result = await pb.collection('players').getOne(id, {
|
||||
expand: 'teams'
|
||||
async getPlayer(id: string): Promise<Player> {
|
||||
const result = await pb.collection("players").getOne(id, {
|
||||
expand: "teams",
|
||||
});
|
||||
return transformPlayer(result);
|
||||
},
|
||||
|
||||
async listPlayers(): Promise<Player[]> {
|
||||
const result = await pb.collection('players').getFullList<Player>({
|
||||
fields: 'id,first_name,last_name'
|
||||
const result = await pb.collection("players").getFullList<Player>({
|
||||
fields: "id,first_name,last_name",
|
||||
});
|
||||
return result.map(transformPlayer);
|
||||
},
|
||||
|
||||
async createPlayer(data: PlayerInput): Promise<Player> {
|
||||
const result = await pb.collection('players').create<Player>(data);
|
||||
const result = await pb.collection("players").create<Player>(data);
|
||||
return transformPlayer(result);
|
||||
},
|
||||
|
||||
async updatePlayer(id: string, data: PlayerUpdateInput): Promise<Player> {
|
||||
const result = await pb.collection('players').update<Player>(id, data);
|
||||
const result = await pb.collection("players").update<Player>(id, data);
|
||||
return transformPlayer(result);
|
||||
},
|
||||
|
||||
async getUnassociatedPlayers(): Promise<Player[]> {
|
||||
const result = await pb.collection('players').getFullList<Player>({
|
||||
const result = await pb.collection("players").getFullList<Player>({
|
||||
filter: 'auth_id = ""',
|
||||
fields: 'id,first_name,last_name'
|
||||
fields: "id,first_name,last_name",
|
||||
});
|
||||
return result.map(transformPlayer);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,15 +6,11 @@ import { Team } from "@/features/teams/types";
|
||||
export function createTeamsService(pb: PocketBase) {
|
||||
return {
|
||||
async getTeam(id: string): Promise<Team | null> {
|
||||
try {
|
||||
logger.info('PocketBase | Getting team', id);
|
||||
const result = await pb.collection('teams').getOne(id, {
|
||||
expand: 'players, tournaments'
|
||||
});
|
||||
return transformTeam(result);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
logger.info("PocketBase | Getting team", id);
|
||||
const result = await pb.collection("teams").getOne(id, {
|
||||
expand: "players, tournaments",
|
||||
});
|
||||
return transformTeam(result);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,15 +12,11 @@ import { transformTeam } from "@/lib/pocketbase/util/transform-types";
|
||||
export function createTournamentsService(pb: PocketBase) {
|
||||
return {
|
||||
async getTournament(id: string): Promise<Tournament | null> {
|
||||
try {
|
||||
logger.info("PocketBase | Getting tournament", id);
|
||||
const result = await pb.collection("tournaments").getOne(id, {
|
||||
expand: "teams, teams.players",
|
||||
});
|
||||
return transformTournament(result);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
logger.info("PocketBase | Getting tournament", id);
|
||||
const result = await pb.collection("tournaments").getOne(id, {
|
||||
expand: "teams, teams.players",
|
||||
});
|
||||
return transformTournament(result);
|
||||
},
|
||||
async listTournaments(): Promise<Tournament[]> {
|
||||
const result = await pb
|
||||
@@ -51,60 +47,67 @@ export function createTournamentsService(pb: PocketBase) {
|
||||
tournamentId: string,
|
||||
teamId: string
|
||||
): Promise<Tournament> {
|
||||
const result = await pb.collection("tournaments").update<Tournament>(
|
||||
tournamentId,
|
||||
{ "teams+": teamId },
|
||||
{ expand: "teams, teams.players" }
|
||||
);
|
||||
|
||||
await pb.collection("teams").update(
|
||||
teamId,
|
||||
{ "tournaments+": tournamentId }
|
||||
);
|
||||
|
||||
const result = await pb
|
||||
.collection("tournaments")
|
||||
.update<Tournament>(
|
||||
tournamentId,
|
||||
{ "teams+": teamId },
|
||||
{ expand: "teams, teams.players" }
|
||||
);
|
||||
|
||||
await pb
|
||||
.collection("teams")
|
||||
.update(teamId, { "tournaments+": tournamentId });
|
||||
|
||||
return transformTournament(result);
|
||||
},
|
||||
async unenrollTeam(
|
||||
tournamentId: string,
|
||||
teamId: string
|
||||
): Promise<Tournament> {
|
||||
const result = await pb.collection("tournaments").update<Tournament>(
|
||||
tournamentId,
|
||||
{ "teams-": teamId },
|
||||
{ expand: "teams, teams.players" }
|
||||
);
|
||||
|
||||
await pb.collection("teams").update(
|
||||
teamId,
|
||||
{ "tournaments-": tournamentId }
|
||||
);
|
||||
|
||||
const result = await pb
|
||||
.collection("tournaments")
|
||||
.update<Tournament>(
|
||||
tournamentId,
|
||||
{ "teams-": teamId },
|
||||
{ expand: "teams, teams.players" }
|
||||
);
|
||||
|
||||
await pb
|
||||
.collection("teams")
|
||||
.update(teamId, { "tournaments-": tournamentId });
|
||||
|
||||
return transformTournament(result);
|
||||
},
|
||||
async getUnenrolledTeams(tournamentId: string): Promise<Team[]> {
|
||||
try {
|
||||
logger.info("PocketBase | Getting unenrolled teams for tournament", tournamentId);
|
||||
const tournament = await pb.collection("tournaments").getOne(tournamentId, {
|
||||
fields: "teams"
|
||||
});
|
||||
|
||||
logger.info(
|
||||
"PocketBase | Getting unenrolled teams for tournament",
|
||||
tournamentId
|
||||
);
|
||||
const tournament = await pb
|
||||
.collection("tournaments")
|
||||
.getOne(tournamentId, {
|
||||
fields: "teams",
|
||||
});
|
||||
|
||||
const enrolledTeamIds = tournament.teams || [];
|
||||
if (enrolledTeamIds.length === 0) {
|
||||
const allTeams = await pb.collection("teams").getFullList({
|
||||
expand: "players"
|
||||
expand: "players",
|
||||
});
|
||||
return allTeams.map(transformTeam);
|
||||
}
|
||||
|
||||
|
||||
const filter = enrolledTeamIds
|
||||
.map((teamId: string) => `id != "${teamId}"`)
|
||||
.join(" && ");
|
||||
|
||||
|
||||
const availableTeams = await pb.collection("teams").getFullList({
|
||||
filter,
|
||||
expand: "players"
|
||||
expand: "players",
|
||||
});
|
||||
|
||||
|
||||
return availableTeams.map(transformTeam);
|
||||
} catch (error) {
|
||||
logger.error("PocketBase | Error getting unenrolled teams", error);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function transformTeam(record: any): Team {
|
||||
new Date(a.created!) < new Date(b.created!) ? -1 : 0
|
||||
)
|
||||
?.map(transformPlayer) ?? [];
|
||||
const tournaments =
|
||||
const tournaments =
|
||||
record.expand?.tournaments
|
||||
?.sort((a: Tournament, b: Tournament) =>
|
||||
new Date(a.created!) < new Date(b.created!) ? -1 : 0
|
||||
@@ -56,7 +56,7 @@ export function transformTeam(record: any): Team {
|
||||
created: record.created,
|
||||
updated: record.updated,
|
||||
players,
|
||||
tournaments
|
||||
tournaments,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { toast as sonnerToast } from 'sonner';
|
||||
import { ToastProps } from './types';
|
||||
import { Notification } from '@mantine/core';
|
||||
import { ShieldCheckIcon, WarningCircleIcon } from '@phosphor-icons/react';
|
||||
import { toast as sonnerToast } from "sonner";
|
||||
import { ToastProps } from "./types";
|
||||
import { Notification } from "@mantine/core";
|
||||
import { ShieldCheckIcon, WarningCircleIcon } from "@phosphor-icons/react";
|
||||
|
||||
const makeToast = (toast: Omit<ToastProps, 'id'>) => {
|
||||
const makeToast = (toast: Omit<ToastProps, "id">) => {
|
||||
return sonnerToast.custom((id) => (
|
||||
<Toast
|
||||
id={id}
|
||||
@@ -14,24 +14,33 @@ const makeToast = (toast: Omit<ToastProps, 'id'>) => {
|
||||
color={toast.color}
|
||||
loading={!!toast.loading}
|
||||
/>
|
||||
))
|
||||
));
|
||||
};
|
||||
|
||||
function success(toast: Omit<ToastProps, "id"> | string) {
|
||||
const config = typeof toast === "string" ? { description: toast } : toast;
|
||||
return makeToast({
|
||||
...config,
|
||||
icon: <ShieldCheckIcon color="lightgreen" size={48} weight="fill" />,
|
||||
});
|
||||
}
|
||||
|
||||
function success(toast: Omit<ToastProps, 'id'> | string) {
|
||||
const config = typeof toast === 'string' ? { description: toast } : toast;
|
||||
return makeToast({ ...config, icon: <ShieldCheckIcon color='lightgreen' size={48} weight='fill'/> });
|
||||
}
|
||||
|
||||
function error(toast: Omit<ToastProps, 'id'> | string) {
|
||||
const config = typeof toast === 'string' ? { description: toast } : toast;
|
||||
return makeToast({ ...config, icon: <WarningCircleIcon color='lightcoral' size={48} weight='fill' /> });
|
||||
function error(toast: Omit<ToastProps, "id"> | string) {
|
||||
const config = typeof toast === "string" ? { description: toast } : toast;
|
||||
return makeToast({
|
||||
...config,
|
||||
icon: <WarningCircleIcon color="lightcoral" size={48} weight="fill" />,
|
||||
});
|
||||
}
|
||||
|
||||
function Toast(props: ToastProps) {
|
||||
const { title, description, withCloseButton, icon, loading } = props;
|
||||
|
||||
return (
|
||||
<Notification miw='md' color={'rgba(0,0,0,0)'} withBorder
|
||||
<Notification
|
||||
miw="md"
|
||||
color={"rgba(0,0,0,0)"}
|
||||
withBorder
|
||||
withCloseButton={!!withCloseButton}
|
||||
loading={loading}
|
||||
title={title}
|
||||
@@ -42,7 +51,7 @@ function Toast(props: ToastProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
export default {
|
||||
success,
|
||||
error,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import SuperTokens from 'supertokens-web-js';
|
||||
import Session from 'supertokens-web-js/recipe/session';
|
||||
import Passwordless from 'supertokens-web-js/recipe/passwordless';
|
||||
import { appInfo } from './config';
|
||||
import { logger } from './';
|
||||
import SuperTokens from "supertokens-web-js";
|
||||
import Session from "supertokens-web-js/recipe/session";
|
||||
import Passwordless from "supertokens-web-js/recipe/passwordless";
|
||||
import { appInfo } from "./config";
|
||||
import { logger } from "./";
|
||||
|
||||
export const frontendConfig = () => {
|
||||
return {
|
||||
@@ -12,27 +12,27 @@ export const frontendConfig = () => {
|
||||
Session.init({
|
||||
tokenTransferMethod: "cookie",
|
||||
sessionTokenBackendDomain: undefined,
|
||||
|
||||
|
||||
preAPIHook: async (context) => {
|
||||
context.requestInit.credentials = "include";
|
||||
return context;
|
||||
},
|
||||
})
|
||||
]
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let initialized = false;
|
||||
export function ensureSuperTokensFrontend() {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
if (!initialized) {
|
||||
SuperTokens.init(frontendConfig());
|
||||
initialized = true;
|
||||
logger.info("Initialized");
|
||||
|
||||
Session.doesSessionExist().then(exists => {
|
||||
logger.info(`Session does${exists ? '' : 'NOT'} exist on load!`);
|
||||
|
||||
Session.doesSessionExist().then((exists) => {
|
||||
logger.info(`Session does${exists ? "" : "NOT"} exist on load!`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Passwordless from "supertokens-node/recipe/passwordless";
|
||||
import { logger } from "../";
|
||||
|
||||
const init = () => (
|
||||
const init = () =>
|
||||
Passwordless.init({
|
||||
flowType: "USER_INPUT_CODE",
|
||||
contactMethod: "PHONE",
|
||||
@@ -14,17 +14,17 @@ const init = () => (
|
||||
throw new Error("No user input code provided to sendSms");
|
||||
}
|
||||
|
||||
logger.info('Sending Code',
|
||||
'######################',
|
||||
'## SuperTokens Code ##',
|
||||
logger.info(
|
||||
"Sending Code",
|
||||
"######################",
|
||||
"## SuperTokens Code ##",
|
||||
`## ${userInputCode} ##`,
|
||||
'######################'
|
||||
"######################"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default { init };
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useSession } from "@tanstack/react-start/server";
|
||||
import Passwordless from "supertokens-node/recipe/passwordless";
|
||||
import { sendVerifyCode, updateVerify } from "@/lib/twilio";
|
||||
|
||||
const init = () => (
|
||||
const init = () =>
|
||||
Passwordless.init({
|
||||
flowType: "USER_INPUT_CODE",
|
||||
contactMethod: "PHONE",
|
||||
@@ -18,22 +18,24 @@ const init = () => (
|
||||
const sid = await sendVerifyCode(phoneNumber, userInputCode);
|
||||
|
||||
const session = await useSession({
|
||||
password: preAuthSessionId
|
||||
password: preAuthSessionId,
|
||||
});
|
||||
|
||||
await session.update({
|
||||
twilioSid: sid
|
||||
twilioSid: sid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
override: {
|
||||
functions: (originalImplementation) => {
|
||||
return {
|
||||
...originalImplementation,
|
||||
consumeCode: async (input) => {
|
||||
const session = await useSession({ password: input.preAuthSessionId });
|
||||
const session = await useSession({
|
||||
password: input.preAuthSessionId,
|
||||
});
|
||||
const twilioSid = session?.data.twilioSid;
|
||||
|
||||
if (!twilioSid) {
|
||||
@@ -46,20 +48,22 @@ const init = () => (
|
||||
await updateVerify(twilioSid);
|
||||
await session.update({
|
||||
twilioSid: undefined,
|
||||
userId: response?.user.id
|
||||
})
|
||||
userId: response?.user.id,
|
||||
});
|
||||
} else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") {
|
||||
if (response.failedCodeInputAttemptCount !== response.maximumCodeInputAttempts) {
|
||||
if (
|
||||
response.failedCodeInputAttemptCount !==
|
||||
response.maximumCodeInputAttempts
|
||||
) {
|
||||
await updateVerify(twilioSid);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default { init };
|
||||
|
||||
@@ -9,6 +9,14 @@ export async function getSessionForStart(request: Request, options?: { sessionRe
|
||||
const session = await getSessionForSSR(request);
|
||||
|
||||
if (session.hasToken) {
|
||||
if (session.accessTokenPayload?.sub === undefined || session.accessTokenPayload?.sessionHandle === undefined) {
|
||||
return {
|
||||
hasToken: true,
|
||||
needsRefresh: true,
|
||||
error: 'TRY_REFRESH_TOKEN'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasToken: true,
|
||||
accessTokenPayload: session.accessTokenPayload,
|
||||
@@ -36,13 +44,3 @@ export async function getSessionForStart(request: Request, options?: { sessionRe
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifySession(request: Request, options?: { sessionRequired?: boolean }) {
|
||||
const session = await getSessionForStart(request, options);
|
||||
|
||||
if (!session && options?.sessionRequired !== false) {
|
||||
throw new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
@@ -9,28 +9,30 @@ import { logger } from "./";
|
||||
|
||||
export const backendConfig = (): TypeInput => {
|
||||
return {
|
||||
framework: 'custom',
|
||||
framework: "custom",
|
||||
supertokens: {
|
||||
connectionURI: import.meta.env.VITE_SUPERTOKENS_URI || "https://try.supertokens.io",
|
||||
connectionURI:
|
||||
import.meta.env.VITE_SUPERTOKENS_URI || "https://try.supertokens.io",
|
||||
},
|
||||
appInfo,
|
||||
recipeList: [
|
||||
PasswordlessDevelopmentMode.init(),
|
||||
Session.init({
|
||||
cookieSameSite: "lax",
|
||||
cookieSecure: process.env.NODE_ENV === 'production',
|
||||
cookieDomain: process.env.NODE_ENV === 'production' ? ".example.com" : undefined,
|
||||
antiCsrf: process.env.NODE_ENV === 'production' ? "VIA_TOKEN" : "NONE",
|
||||
|
||||
cookieSecure: process.env.NODE_ENV === "production",
|
||||
cookieDomain:
|
||||
process.env.NODE_ENV === "production" ? ".example.com" : undefined,
|
||||
antiCsrf: process.env.NODE_ENV === "production" ? "VIA_TOKEN" : "NONE",
|
||||
|
||||
// Debug only
|
||||
exposeAccessTokenToFrontendInCookieBasedAuth: true,
|
||||
}),
|
||||
Dashboard.init(),
|
||||
UserRoles.init()
|
||||
UserRoles.init(),
|
||||
],
|
||||
telemetry: process.env.NODE_ENV !== 'production',
|
||||
telemetry: process.env.NODE_ENV !== "production",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let initialized = false;
|
||||
export function ensureSuperTokensBackend() {
|
||||
|
||||
4
src/lib/tanstack-query/hooks/index.ts
Normal file
4
src/lib/tanstack-query/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './use-optimistic-mutation';
|
||||
export * from './use-server-mutation';
|
||||
export * from './use-server-query';
|
||||
export * from './user-server-suspense-query';
|
||||
37
src/lib/tanstack-query/hooks/use-optimistic-mutation.ts
Normal file
37
src/lib/tanstack-query/hooks/use-optimistic-mutation.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useServerMutation } from "./use-server-mutation";
|
||||
|
||||
export function useOptimisticMutation<TData, TVariables = unknown>(
|
||||
options: Parameters<typeof useServerMutation<TData, TVariables>>[0] & {
|
||||
queryKey: readonly (string | number)[];
|
||||
optimisticUpdate?: (oldData: any, variables: TVariables) => any;
|
||||
}
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { queryKey, optimisticUpdate, ...mutationOptions } = options;
|
||||
|
||||
return useServerMutation({
|
||||
...mutationOptions,
|
||||
onMutate: async (variables) => {
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
const previousData = queryClient.getQueryData(queryKey);
|
||||
|
||||
if (optimisticUpdate && previousData) {
|
||||
queryClient.setQueryData(queryKey, (old: any) => optimisticUpdate(old, variables));
|
||||
}
|
||||
|
||||
return { previousData };
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (context && typeof context === 'object' && 'previousData' in context && context.previousData) {
|
||||
queryClient.setQueryData(queryKey, context.previousData);
|
||||
}
|
||||
mutationOptions.onError?.(error, variables, context);
|
||||
},
|
||||
onSettled: (data, error, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
mutationOptions.onSettled?.(data, error, variables, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
47
src/lib/tanstack-query/hooks/use-server-mutation.ts
Normal file
47
src/lib/tanstack-query/hooks/use-server-mutation.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
|
||||
import { ServerResult } from "../types";
|
||||
import toast from '@/lib/sonner'
|
||||
|
||||
export function useServerMutation<TData, TVariables = unknown>(
|
||||
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
|
||||
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
|
||||
successMessage?: string;
|
||||
showErrorToast?: boolean;
|
||||
showSuccessToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
mutationFn,
|
||||
successMessage,
|
||||
showErrorToast = true,
|
||||
showSuccessToast = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
...mutationOptions
|
||||
} = options;
|
||||
|
||||
return useMutation({
|
||||
...mutationOptions,
|
||||
mutationFn: async (variables: TVariables) => {
|
||||
const result = await mutationFn(variables);
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
onSuccess: (data, variables, context) => {
|
||||
if (showSuccessToast && successMessage) {
|
||||
toast.success(successMessage);
|
||||
}
|
||||
onSuccess?.(data, variables, context);
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
onError?.(error, variables, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
31
src/lib/tanstack-query/hooks/use-server-query.ts
Normal file
31
src/lib/tanstack-query/hooks/use-server-query.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
import { ServerResult } from "../types";
|
||||
import toast from '@/lib/sonner'
|
||||
|
||||
export function useServerQuery<TData>(
|
||||
options: {
|
||||
queryKey: QueryKey,
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
options?: Omit<UseQueryOptions<TData, Error, TData>, 'queryFn' | 'queryKey'>
|
||||
showErrorToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const { queryKey, queryFn, showErrorToast = true, options: queryOptions } = options;
|
||||
|
||||
return useQuery({
|
||||
...queryOptions,
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
const result = await queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { ServerResult } from '@/lib/tanstack-query/types';
|
||||
|
||||
export function useServerQuery<TData>(
|
||||
options: Omit<UseQueryOptions<TData, Error, TData>, 'queryFn'> & {
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
showErrorToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const { queryFn, showErrorToast = true, ...queryOptions } = options;
|
||||
|
||||
return useQuery({
|
||||
...queryOptions,
|
||||
queryFn: async () => {
|
||||
const result = await queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useServerSuspenseQuery<TData>(
|
||||
options: Omit<UseQueryOptions<TData, Error, TData>, 'queryFn'> & {
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
showErrorToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const { queryFn, showErrorToast = true, ...queryOptions } = options;
|
||||
|
||||
return useSuspenseQuery({
|
||||
...queryOptions,
|
||||
queryFn: async () => {
|
||||
const result = await queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useServerMutation<TData, TVariables = unknown>(
|
||||
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
|
||||
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
|
||||
successMessage?: string;
|
||||
showErrorToast?: boolean;
|
||||
showSuccessToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
mutationFn,
|
||||
successMessage,
|
||||
showErrorToast = true,
|
||||
showSuccessToast = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
...mutationOptions
|
||||
} = options;
|
||||
|
||||
return useMutation({
|
||||
...mutationOptions,
|
||||
mutationFn: async (variables: TVariables) => {
|
||||
const result = await mutationFn(variables);
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
onSuccess: (data, variables, context) => {
|
||||
if (showSuccessToast && successMessage) {
|
||||
toast.success(successMessage);
|
||||
}
|
||||
onSuccess?.(data, variables, context);
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
onError?.(error, variables, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useOptimisticMutation<TData, TVariables = unknown>(
|
||||
options: Parameters<typeof useServerMutation<TData, TVariables>>[0] & {
|
||||
queryKey: readonly (string | number)[];
|
||||
optimisticUpdate?: (oldData: any, variables: TVariables) => any;
|
||||
}
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { queryKey, optimisticUpdate, ...mutationOptions } = options;
|
||||
|
||||
return useServerMutation({
|
||||
...mutationOptions,
|
||||
onMutate: async (variables) => {
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
const previousData = queryClient.getQueryData(queryKey);
|
||||
|
||||
if (optimisticUpdate && previousData) {
|
||||
queryClient.setQueryData(queryKey, (old: any) => optimisticUpdate(old, variables));
|
||||
}
|
||||
|
||||
return { previousData };
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (context && typeof context === 'object' && 'previousData' in context && context.previousData) {
|
||||
queryClient.setQueryData(queryKey, context.previousData);
|
||||
}
|
||||
mutationOptions.onError?.(error, variables, context);
|
||||
},
|
||||
onSettled: (data, error, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
mutationOptions.onSettled?.(data, error, variables, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
32
src/lib/tanstack-query/hooks/user-server-suspense-query.ts
Normal file
32
src/lib/tanstack-query/hooks/user-server-suspense-query.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { QueryKey, UseQueryOptions, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { ServerResult } from "../types";
|
||||
import toast from '@/lib/sonner'
|
||||
|
||||
export function useServerSuspenseQuery<TData>(
|
||||
options: {
|
||||
queryKey: QueryKey,
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
options?: Omit<UseQueryOptions<TData, Error, TData>, 'queryFn' | 'queryKey'>
|
||||
showErrorToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const { queryKey, queryFn, showErrorToast = true, options: queryOptions } = options;
|
||||
const queryResult = useSuspenseQuery({
|
||||
...queryOptions,
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
const result = await queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
|
||||
return queryResult;
|
||||
}
|
||||
23
src/lib/tanstack-query/utils/ensure.ts
Normal file
23
src/lib/tanstack-query/utils/ensure.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||
import { ServerResult } from "../types";
|
||||
|
||||
export async function ensureServerQueryData<TData>(
|
||||
queryClient: QueryClient,
|
||||
query: {
|
||||
queryKey: QueryKey;
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
}
|
||||
): Promise<TData> {
|
||||
return queryClient.ensureQueryData({
|
||||
queryKey: query.queryKey,
|
||||
queryFn: async () => {
|
||||
const result = await query.queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
23
src/lib/tanstack-query/utils/prefetch.ts
Normal file
23
src/lib/tanstack-query/utils/prefetch.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { QueryClient, QueryKey } from '@tanstack/react-query';
|
||||
import { ServerResult } from '../types';
|
||||
|
||||
export async function prefetchServerQuery<TData>(
|
||||
queryClient: QueryClient,
|
||||
query: {
|
||||
queryKey: QueryKey;
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
}
|
||||
): Promise<void> {
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: query.queryKey,
|
||||
queryFn: async () => {
|
||||
const result = await query.queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { logger } from "../logger";
|
||||
import { ErrorType, ServerError, ServerResult } from "./types";
|
||||
import { logger } from "../../logger";
|
||||
import { ErrorType, ServerError, ServerResult } from "../types";
|
||||
|
||||
export const createServerError = (
|
||||
type: ErrorType,
|
||||
@@ -15,30 +15,19 @@ export const createServerError = (
|
||||
context,
|
||||
});
|
||||
|
||||
export const withErrorHandling = async <T>(
|
||||
operation: () => Promise<T>,
|
||||
context: string,
|
||||
customErrorMap?: (error: unknown) => ServerError | null
|
||||
): Promise<ServerResult<T>> => {
|
||||
export const toServerResult = async <T>(serverFn: () => Promise<T>): Promise<ServerResult<T>> => {
|
||||
try {
|
||||
const data = await operation();
|
||||
const data = await serverFn();
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
logger.error(`${context} failed`, { error, context });
|
||||
logger.error('Server Fn Error', error);
|
||||
|
||||
if (customErrorMap) {
|
||||
const customError = customErrorMap(error);
|
||||
if (customError) {
|
||||
return { success: false, error: customError };
|
||||
}
|
||||
}
|
||||
|
||||
const mappedError = mapKnownError(error, context);
|
||||
const mappedError = mapKnownError(error);
|
||||
return { success: false, error: mappedError };
|
||||
}
|
||||
};
|
||||
|
||||
const mapKnownError = (error: unknown, context: string): ServerError => {
|
||||
const mapKnownError = (error: unknown): ServerError => {
|
||||
if (error && typeof error === "object" && "status" in error) {
|
||||
const pbError = error as {
|
||||
status: number;
|
||||
@@ -100,7 +89,7 @@ const mapKnownError = (error: unknown, context: string): ServerError => {
|
||||
error.message,
|
||||
"An unexpected error occurred. Please try again.",
|
||||
undefined,
|
||||
{ stack: error.stack, context }
|
||||
{ stack: error.stack }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,6 +98,6 @@ const mapKnownError = (error: unknown, context: string): ServerError => {
|
||||
String(error),
|
||||
"An unexpected error occurred. Please try again.",
|
||||
undefined,
|
||||
{ originalError: error, context }
|
||||
{ originalError: error }
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,19 @@
|
||||
import twilio from "twilio";
|
||||
|
||||
const accountSid = import.meta.env.VITE_TWILIO_ACCOUNT_SID
|
||||
const authToken = import.meta.env.VITE_TWILIO_AUTH_TOKEN
|
||||
const serviceSid = import.meta.env.VITE_TWILIO_SERVICE_SID
|
||||
const accountSid = import.meta.env.VITE_TWILIO_ACCOUNT_SID;
|
||||
const authToken = import.meta.env.VITE_TWILIO_AUTH_TOKEN;
|
||||
const serviceSid = import.meta.env.VITE_TWILIO_SERVICE_SID;
|
||||
|
||||
const client = twilio(accountSid, authToken);
|
||||
|
||||
export async function sendVerifyCode(phoneNumber: string, code: string) {
|
||||
const verification = await client.verify.v2
|
||||
.services(serviceSid)
|
||||
.verifications.create({
|
||||
channel: "sms",
|
||||
customCode: code,
|
||||
to: phoneNumber,
|
||||
});
|
||||
.services(serviceSid)
|
||||
.verifications.create({
|
||||
channel: "sms",
|
||||
customCode: code,
|
||||
to: phoneNumber,
|
||||
});
|
||||
|
||||
if (verification.status !== "pending") {
|
||||
throw new Error("Unknown error sending verification code");
|
||||
|
||||
Reference in New Issue
Block a user