feat: implement QuestionBank CRUD with pagination and template query
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import ConfirmDialog from '../components/ConfirmDialog';
|
||||
|
||||
interface ConfirmOptions {
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
}
|
||||
|
||||
interface ConfirmContextType {
|
||||
confirm: (options: ConfirmOptions | string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const ConfirmContext = createContext<ConfirmContextType | undefined>(undefined);
|
||||
|
||||
export const useConfirm = () => {
|
||||
const context = useContext(ConfirmContext);
|
||||
if (!context) {
|
||||
throw new Error('useConfirm must be used within a ConfirmProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface ConfirmProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ConfirmProvider: React.FC<ConfirmProviderProps> = ({ children }) => {
|
||||
const [options, setOptions] = useState<ConfirmOptions | null>(null);
|
||||
const [resolveRef, setResolveRef] = useState<((value: boolean) => void) | null>(null);
|
||||
|
||||
const confirm = (opts: ConfirmOptions | string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
if (typeof opts === 'string') {
|
||||
setOptions({ message: opts });
|
||||
} else {
|
||||
setOptions(opts);
|
||||
}
|
||||
setResolveRef(() => resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (resolveRef) resolveRef(true);
|
||||
setOptions(null);
|
||||
setResolveRef(null);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (resolveRef) resolveRef(false);
|
||||
setOptions(null);
|
||||
setResolveRef(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmContext.Provider value={{ confirm }}>
|
||||
{children}
|
||||
{options && (
|
||||
<ConfirmDialog
|
||||
isOpen={!!options}
|
||||
title={options.title}
|
||||
message={options.message}
|
||||
confirmLabel={options.confirmLabel}
|
||||
cancelLabel={options.cancelLabel}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</ConfirmContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { translations, Language } from '../utils/translations';
|
||||
|
||||
interface LanguageContextType {
|
||||
language: Language;
|
||||
setLanguage: (lang: Language) => void;
|
||||
t: (key: keyof typeof translations['zh'], ...args: any[]) => string;
|
||||
}
|
||||
|
||||
const LanguageContext = React.createContext<LanguageContextType | undefined>(undefined);
|
||||
|
||||
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [language, setLanguage] = React.useState<Language>(() => {
|
||||
const saved = localStorage.getItem('userLanguage');
|
||||
return (saved as Language) || 'zh';
|
||||
});
|
||||
|
||||
const handleSetLanguage = (lang: Language) => {
|
||||
setLanguage(lang);
|
||||
localStorage.setItem('userLanguage', lang);
|
||||
};
|
||||
|
||||
const t = (key: keyof typeof translations['zh'], ...args: any[]) => {
|
||||
let str = translations[language][key] || key;
|
||||
if (args.length > 0) {
|
||||
args.forEach((arg, i) => {
|
||||
str = str.replace(new RegExp(`\\$${i + 1}`, 'g'), String(arg));
|
||||
});
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
LanguageContext.Provider,
|
||||
{ value: { language, setLanguage: handleSetLanguage, t } },
|
||||
children
|
||||
);
|
||||
};
|
||||
|
||||
export const useLanguage = () => {
|
||||
const context = React.useContext(LanguageContext);
|
||||
if (!context) {
|
||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
||||
import Toast, { ToastType } from '../components/Toast';
|
||||
import { registerToastHandler } from '../src/utils/toast';
|
||||
|
||||
interface ToastItem {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
title?: string;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface ToastContextType {
|
||||
showToast: (type: ToastType, message: string, title?: string, duration?: number) => void;
|
||||
showSuccess: (message: string, title?: string) => void;
|
||||
showError: (message: string, title?: string) => void;
|
||||
showWarning: (message: string, title?: string) => void;
|
||||
showInfo: (message: string, title?: string) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
|
||||
export const useToast = () => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface ToastProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
||||
|
||||
const showToast = (type: ToastType, message: string, title?: string, duration?: number) => {
|
||||
const id = Date.now().toString();
|
||||
const newToast: ToastItem = { id, type, message, title, duration };
|
||||
|
||||
setToasts(prev => {
|
||||
// Deduplicate identical messages: discard old one if current type and content are the same
|
||||
const filtered = prev.filter(t => t.message !== message || t.type !== type);
|
||||
return [...filtered, newToast];
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
registerToastHandler({ showToast });
|
||||
}, []);
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
};
|
||||
|
||||
const showSuccess = (message: string, title?: string) => showToast('success', message, title);
|
||||
const showError = (message: string, title?: string) => showToast('error', message, title);
|
||||
const showWarning = (message: string, title?: string) => showToast('warning', message, title);
|
||||
const showInfo = (message: string, title?: string) => showToast('info', message, title);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast, showSuccess, showError, showWarning, showInfo }}>
|
||||
{children}
|
||||
<div className="fixed bottom-0 right-0 z-[9999] p-4 space-y-3 pointer-events-none w-full max-w-sm flex flex-col items-end">
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className="pointer-events-auto w-full"
|
||||
>
|
||||
<Toast
|
||||
type={toast.type}
|
||||
title={toast.title}
|
||||
message={toast.message}
|
||||
duration={toast.duration}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user