Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion client/src/components/settings/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface AccountSettingsProps {
userRole?: string;
language: string;
setLanguage: (value: string) => void;
dateFormat: string;
setDateFormat: (value: string) => void;
}

const AccountSettings = ({
Expand All @@ -27,7 +29,9 @@ const AccountSettings = ({
minecraftUsername,
userRole,
language,
setLanguage
setLanguage,
dateFormat,
setDateFormat
}: AccountSettingsProps) => {
const { toast } = useToast();
const { logout } = useAuth();
Expand Down Expand Up @@ -120,13 +124,33 @@ const AccountSettings = ({
<SelectItem value="en">English</SelectItem>
<SelectItem value="de">Deutsch</SelectItem>
<SelectItem value="es">Español</SelectItem>
<SelectItem value="nl">Nederlands</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground mt-1.5">
{t('settings.languageDescription')}
</p>
</div>
</div>

<div className="flex gap-3">
<Label htmlFor="date-format" className="w-36 text-sm pt-2.5 shrink-0">{t('settings.dateFormat')}</Label>
<div className="flex-1 max-w-xs">
<Select value={dateFormat} onValueChange={setDateFormat}>
<SelectTrigger id="date-format">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="MM/DD/YYYY">MM/DD/YYYY HH:mm</SelectItem>
<SelectItem value="DD/MM/YYYY">DD/MM/YYYY HH:mm</SelectItem>
<SelectItem value="YYYY-MM-DD">YYYY-MM-DD HH:mm</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground mt-1.5">
{t('settings.dateFormatDescription')}
</p>
</div>
</div>
</div>
</div>
);
Expand Down
68 changes: 29 additions & 39 deletions client/src/components/windows/PlayerWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1052,13 +1052,20 @@ const PlayerWindow = ({ playerId, isOpen, onClose, initialPosition }: PlayerWind

// Wait a bit for the tab to render, then scroll to the punishment
setTimeout(() => {
const punishmentElement = document.querySelector(`[data-punishment-id="${punishmentIdParam}"]`);
const punishmentElement = document.querySelector(`[data-punishment-id="${punishmentIdParam}"]`) as HTMLElement;
if (punishmentElement) {
punishmentElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});

// Find the scrollable content container (overflow-y-auto parent) instead of using scrollIntoView
const scrollContainer = punishmentElement.closest('.overflow-y-auto');
if (scrollContainer) {
const containerRect = scrollContainer.getBoundingClientRect();
const elementRect = punishmentElement.getBoundingClientRect();
const offsetTop = elementRect.top - containerRect.top + scrollContainer.scrollTop;
scrollContainer.scrollTo({
top: offsetTop - containerRect.height / 2 + elementRect.height / 2,
behavior: 'smooth'
});
}

// Add a highlight effect
punishmentElement.classList.add('bg-blue-100', 'border-2', 'border-blue-400');
setTimeout(() => {
Expand Down Expand Up @@ -1326,45 +1333,28 @@ const PlayerWindow = ({ playerId, isOpen, onClose, initialPosition }: PlayerWind

// Apply modifications in chronological order
const sortedModifications = modifications.sort((a: any, b: any) => {
const dateA = a.issued ? new Date(a.issued) : new Date(0);
const dateB = b.issued ? new Date(b.issued) : new Date(0);
return dateA.getTime() - dateB.getTime();
const dateA = a.date ? new Date(a.date).getTime() : 0;
const dateB = b.date ? new Date(b.date).getTime() : 0;
return dateA - dateB;
});

for (const mod of sortedModifications) {
if (mod.type === 'MANUAL_PARDON' || mod.type === 'APPEAL_ACCEPT') {
effectiveActive = false; } else if (mod.type === 'MANUAL_DURATION_CHANGE' || mod.type === 'APPEAL_DURATION_CHANGE') {
effectiveActive = false;
} else if (mod.type === 'MANUAL_DURATION_CHANGE' || mod.type === 'APPEAL_DURATION_CHANGE') {
if (mod.effectiveDuration !== undefined) {
effectiveDuration = mod.effectiveDuration;

// For duration modifications, calculate expiry from the modification's issued time
const modificationTime = mod.issued;

// Convert modificationTime to Date object if it's a string
let modDate;
if (modificationTime instanceof Date) {
modDate = modificationTime;
} else if (typeof modificationTime === 'string') {
modDate = new Date(modificationTime);
} else {
// Fallback to current date if modificationTime is invalid
console.warn('Invalid modification time, using current date as fallback:', modificationTime);
modDate = new Date();
}

// Validate the modDate
if (isNaN(modDate.getTime())) {
console.warn('Invalid modification date calculated, using current date as fallback:', modDate);
modDate = new Date();
}
if (mod.effectiveDuration === 0 || mod.effectiveDuration === -1 || mod.effectiveDuration < 0) {

if (mod.effectiveDuration === 0 || mod.effectiveDuration === -1 || mod.effectiveDuration < 0) {
effectiveExpiry = null; // Permanent
effectiveActive = true; // Permanent punishments are always active
} else {
effectiveExpiry = new Date(modDate.getTime() + mod.effectiveDuration);
// Update active status based on whether the new expiry is in the future
const now = new Date();
effectiveActive = effectiveExpiry.getTime() > now.getTime();
} else if (mod.date) {
const modDate = new Date(mod.date);
if (!isNaN(modDate.getTime())) {
effectiveExpiry = new Date(modDate.getTime() + mod.effectiveDuration);
const now = new Date();
effectiveActive = effectiveExpiry.getTime() > now.getTime();
}
}
}
}
Expand Down Expand Up @@ -2098,7 +2088,7 @@ const PlayerWindow = ({ playerId, isOpen, onClose, initialPosition }: PlayerWind
{mod.type.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, (l: string) => l.toUpperCase())}
</Badge>
<span className="text-muted-foreground text-xs">
{formatDateWithTime(mod.issued)}
{formatDateWithTime(mod.date)}
</span>
</div>
{mod.reason && (
Expand Down
24 changes: 20 additions & 4 deletions client/src/hooks/use-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createContext, ReactNode, useContext, useState, useEffect } from "react
import { useLocation } from "wouter";
import { useToast } from "@modl-gg/shared-web/hooks/use-toast";
import { getApiUrl, getCurrentDomain } from "@/lib/api";
import { setDateLocale } from "@/utils/date-utils";
import { setDateLocale, setDateFormat } from "@/utils/date-utils";
import i18n from "@/lib/i18n";

interface User {
Expand All @@ -12,6 +12,7 @@ interface User {
role: 'Super Admin' | 'Admin' | 'Moderator' | 'Helper';
minecraftUsername?: string; // The staff's Minecraft username, used for punishment issuerName
language?: string;
dateFormat?: string;
}

type AuthContextType = {
Expand Down Expand Up @@ -45,6 +46,7 @@ function mapUserFromMeResponse(userData: any): User {
role: userData.role,
minecraftUsername: userData.minecraftUsername,
language: userData.language || 'en',
dateFormat: userData.dateFormat || 'MM/DD/YYYY',
};
}

Expand All @@ -64,8 +66,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return mapUserFromMeResponse(userData);
};

// Check for existing session on mount
// Check for existing session on mount (skip on public pages)
useEffect(() => {
const path = window.location.pathname;
const isPublicPage = path.startsWith('/ticket/') ||
path.startsWith('/appeal') ||
path.startsWith('/submit-ticket') ||
path === '/' ||
path.startsWith('/knowledgebase') ||
path.startsWith('/article/');

if (isPublicPage) {
setIsLoading(false);
return;
}

const checkSession = async () => {
try {
const authenticatedUser = await fetchAuthenticatedUser();
Expand All @@ -82,12 +97,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
checkSession();
}, []);

// Sync date locale and i18n language when user language changes
// Sync date locale, date format, and i18n language when user settings change
useEffect(() => {
const lang = user?.language || 'en';
setDateLocale(lang);
setDateFormat(user?.dateFormat || 'MM/DD/YYYY');
i18n.changeLanguage(lang);
}, [user?.language]);
}, [user?.language, user?.dateFormat]);

const requestEmailVerification = async (email: string): Promise<string | undefined> => {
try {
Expand Down
100 changes: 95 additions & 5 deletions client/src/hooks/use-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,23 +128,49 @@ export function useTicket(id: string) {
return useQuery({
queryKey: ['/v1/public/tickets', id],
queryFn: async () => {
const res = await apiFetch(`/v1/public/tickets/${id}`);
// Check for stored ticket auth token
const tokenKey = `ticket_auth_${id}`;
const token = getCookie(tokenKey);

const url = token
? `/v1/public/tickets/${id}?token=${encodeURIComponent(token)}`
: `/v1/public/tickets/${id}`;

const res = await apiFetch(url);
if (!res.ok) {
if (res.status === 404) {
return null;
}
if (res.status === 403) {
const data = await res.json().catch(() => ({ requiresVerification: true }));
if (data.requiresVerification) {
return { requiresVerification: true, emailHint: data.emailHint || '', ticketId: id };
}
}
throw new Error('Failed to fetch ticket');
}
return res.json();
},
enabled: !!id,
retry: false,
staleTime: 0,
gcTime: 0,
refetchOnMount: true,
refetchOnWindowFocus: true
});
}

// Cookie helpers for ticket auth tokens
export function setCookie(name: string, value: string, days: number) {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
}

function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? decodeURIComponent(match[2]) : null;
}

export function useCreateTicket() {
return useMutation({
mutationFn: async (ticketData: any) => {
Expand Down Expand Up @@ -195,11 +221,16 @@ export function useUpdateTicket() {
export function useAddTicketReply() {
return useMutation({
mutationFn: async ({ id, reply }: { id: string, reply: any }) => {
const res = await apiFetch(`/v1/public/tickets/${id}/replies`, {
const tokenKey = `ticket_auth_${id}`;
const token = getCookie(tokenKey);

const url = token
? `/v1/public/tickets/${id}/replies?token=${encodeURIComponent(token)}`
: `/v1/public/tickets/${id}/replies`;

const res = await apiFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reply)
});

Expand Down Expand Up @@ -238,6 +269,47 @@ export function useSubmitTicketForm() {
});
}

export function useRequestTicketVerification() {
return useMutation({
mutationFn: async (ticketId: string) => {
const res = await apiFetch(`/v1/public/tickets/${ticketId}/request-verification`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});

if (!res.ok) {
const error = await res.json().catch(() => ({}));
throw new Error(error.message || 'Failed to request verification');
}

return res.json();
}
});
}

export function useVerifyTicketCode() {
return useMutation({
mutationFn: async ({ ticketId, code }: { ticketId: string, code: string }) => {
const res = await apiFetch(`/v1/public/tickets/${ticketId}/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
});

if (!res.ok) {
const error = await res.json().catch(() => ({}));
throw new Error(error.message || 'Invalid or expired code');
}

return res.json();
}
});
}

export function useAppeals() {
return useQuery({
queryKey: ['/v1/panel/appeals'],
Expand Down Expand Up @@ -488,6 +560,24 @@ export function useQuickResponses() {
});
}

export function useStatusThresholds() {
return useQuery({
queryKey: ['/v1/panel/settings/status-thresholds'],
queryFn: async () => {
const res = await apiFetch('/v1/panel/settings/status-thresholds');
if (!res.ok) {
if (res.status === 401 || res.status === 403) {
return null;
}
throw new Error('Failed to fetch status thresholds');
}
return res.json();
},
staleTime: 1000 * 60 * 5,
refetchOnWindowFocus: false
});
}

export function useStats() {
return useQuery({
queryKey: ['/v1/panel/stats'],
Expand Down
1 change: 1 addition & 0 deletions client/src/hooks/use-permissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export function usePermissions() {
if (!response.ok) throw new Error('Failed to fetch roles');
return response.json();
},
enabled: !!user,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});

Expand Down
Loading