fa
This commit is contained in:
44
src/lib/stats.ts
Normal file
44
src/lib/stats.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || process.cwd();
|
||||
const STATS_FILE = path.join(DATA_DIR, 'stats.json');
|
||||
|
||||
export interface Stats {
|
||||
pv: number;
|
||||
uv: number;
|
||||
visitors: string[]; // Store hashed IPs or session IDs
|
||||
}
|
||||
|
||||
export const getStats = (): Stats => {
|
||||
try {
|
||||
if (!fs.existsSync(STATS_FILE)) {
|
||||
const initialStats: Stats = { pv: 0, uv: 0, visitors: [] };
|
||||
fs.writeFileSync(STATS_FILE, JSON.stringify(initialStats, null, 2));
|
||||
return initialStats;
|
||||
}
|
||||
const data = fs.readFileSync(STATS_FILE, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error('[Stats] Error reading stats:', error);
|
||||
return { pv: 0, uv: 0, visitors: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const recordHit = (visitorId: string): Stats => {
|
||||
try {
|
||||
const stats = getStats();
|
||||
stats.pv += 1;
|
||||
|
||||
if (!stats.visitors.includes(visitorId)) {
|
||||
stats.visitors.push(visitorId);
|
||||
stats.uv = stats.visitors.length;
|
||||
}
|
||||
|
||||
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2));
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[Stats] Error recording hit:', error);
|
||||
return getStats();
|
||||
}
|
||||
};
|
||||
332
src/lib/storage.ts
Normal file
332
src/lib/storage.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Todo, Group, Stats, DEFAULT_GROUP_ID } from './types';
|
||||
|
||||
// 支持 Docker 数据目录 and 本地开发
|
||||
const DATA_DIR = process.env.DATA_DIR || process.cwd();
|
||||
const DATA_FILE = path.join(DATA_DIR, 'todos.json');
|
||||
const STATS_FILE = path.join(DATA_DIR, 'stats.json');
|
||||
const GROUPS_FILE = path.join(DATA_DIR, 'groups.json');
|
||||
const RATE_LIMITS_FILE = path.join(DATA_DIR, 'rate-limits.json');
|
||||
|
||||
// 限流数据结构
|
||||
export interface RateLimitData {
|
||||
lastAddTime: number; // 上次添加任务的时间戳
|
||||
dailyCount: number; // 当天已添加的任务数
|
||||
lastResetDate: string; // 上次重置日期 (YYYY-MM-DD)
|
||||
history: number[]; // 添加任务的历史时间戳
|
||||
}
|
||||
|
||||
export interface RateLimits {
|
||||
[clientId: string]: RateLimitData;
|
||||
}
|
||||
|
||||
// 限流配置
|
||||
export const RATE_LIMIT_CONFIG = {
|
||||
INTERVAL_MS: 10 * 60 * 1000, // 10分钟
|
||||
DAILY_MAX: 6, // 每天6个任务
|
||||
};
|
||||
|
||||
export { DEFAULT_GROUP_ID };
|
||||
export type { Todo, Group, Stats };
|
||||
|
||||
export const getTodos = (): Todo[] => {
|
||||
try {
|
||||
// 确保目录存在
|
||||
const dir = path.dirname(DATA_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// 如果文件不存在,创建空数组
|
||||
if (!fs.existsSync(DATA_FILE)) {
|
||||
fs.writeFileSync(DATA_FILE, '[]', 'utf-8');
|
||||
console.log(`[Storage] Created new todos file: ${DATA_FILE}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(DATA_FILE, 'utf-8');
|
||||
let todos: Todo[] = JSON.parse(data);
|
||||
|
||||
// 迁移逻辑:确保所有任务都有 groupId,如果没有则归入默认分组
|
||||
let needsSave = false;
|
||||
todos = todos.map(todo => {
|
||||
if (!todo.groupId) {
|
||||
todo.groupId = DEFAULT_GROUP_ID;
|
||||
needsSave = true;
|
||||
}
|
||||
return todo;
|
||||
});
|
||||
|
||||
if (needsSave) {
|
||||
saveTodos(todos);
|
||||
}
|
||||
|
||||
console.log(`[Storage] Loaded ${todos.length} todos from ${DATA_FILE}`);
|
||||
return todos;
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error reading todos:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveTodos = (todos: Todo[]) => {
|
||||
try {
|
||||
// 确保目录存在
|
||||
const dir = path.dirname(DATA_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2), 'utf-8');
|
||||
console.log(`[Storage] Saved ${todos.length} todos to ${DATA_FILE}`);
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error saving todos:', error);
|
||||
throw error; // 抛出错误以便 API 能捕获
|
||||
}
|
||||
};
|
||||
|
||||
export const getGroups = (): Group[] => {
|
||||
try {
|
||||
const dir = path.dirname(GROUPS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(GROUPS_FILE)) {
|
||||
const defaultGroups: Group[] = [{ id: DEFAULT_GROUP_ID, name: 'Default', createdAt: Date.now() }];
|
||||
fs.writeFileSync(GROUPS_FILE, JSON.stringify(defaultGroups, null, 2), 'utf-8');
|
||||
return defaultGroups;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(GROUPS_FILE, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error reading groups:', error);
|
||||
return [{ id: DEFAULT_GROUP_ID, name: 'Default', createdAt: Date.now() }];
|
||||
}
|
||||
};
|
||||
|
||||
export const saveGroups = (groups: Group[]) => {
|
||||
try {
|
||||
const dir = path.dirname(GROUPS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(GROUPS_FILE, JSON.stringify(groups, null, 2), 'utf-8');
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error saving groups:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStats = (): Stats => {
|
||||
try {
|
||||
// 确保数据目录存在
|
||||
const dir = path.dirname(STATS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(`[Storage] Created data directory: ${dir}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(STATS_FILE)) {
|
||||
const initialStats = { pv: 0, uv: 0 };
|
||||
fs.writeFileSync(STATS_FILE, JSON.stringify(initialStats, null, 2), 'utf-8');
|
||||
console.log(`[Storage] Created new stats file: ${STATS_FILE}`);
|
||||
return initialStats;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(STATS_FILE, 'utf-8');
|
||||
const stats = JSON.parse(data);
|
||||
console.log(`[Storage] Loaded stats from ${STATS_FILE}: PV=${stats.pv}, UV=${stats.uv}`);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error reading stats:', error);
|
||||
return { pv: 0, uv: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
export const updateStats = (isNewVisitor: boolean): Stats => {
|
||||
try {
|
||||
// 确保数据目录存在
|
||||
const dir = path.dirname(STATS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const stats = getStats();
|
||||
stats.pv += 1;
|
||||
if (isNewVisitor) {
|
||||
stats.uv += 1;
|
||||
}
|
||||
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2), 'utf-8');
|
||||
console.log(`[Storage] Updated stats: PV=${stats.pv}, UV=${stats.uv}`);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error updating stats:', error);
|
||||
return { pv: 0, uv: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
// ============ 限流管理函数 ============
|
||||
|
||||
/**
|
||||
* 获取所有限流数据
|
||||
*/
|
||||
export const getRateLimits = (): RateLimits => {
|
||||
try {
|
||||
const dir = path.dirname(RATE_LIMITS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(RATE_LIMITS_FILE)) {
|
||||
const initialData: RateLimits = {};
|
||||
fs.writeFileSync(RATE_LIMITS_FILE, JSON.stringify(initialData, null, 2), 'utf-8');
|
||||
console.log(`[Storage] Created new rate limits file: ${RATE_LIMITS_FILE}`);
|
||||
return initialData;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(RATE_LIMITS_FILE, 'utf-8');
|
||||
const rateLimits = JSON.parse(data);
|
||||
console.log(`[Storage] Loaded rate limits for ${Object.keys(rateLimits).length} clients`);
|
||||
return rateLimits;
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error reading rate limits:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存限流数据
|
||||
*/
|
||||
export const saveRateLimits = (rateLimits: RateLimits): void => {
|
||||
try {
|
||||
const dir = path.dirname(RATE_LIMITS_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(RATE_LIMITS_FILE, JSON.stringify(rateLimits, null, 2), 'utf-8');
|
||||
console.log(`[Storage] Saved rate limits for ${Object.keys(rateLimits).length} clients`);
|
||||
} catch (error) {
|
||||
console.error('[Storage] Error saving rate limits:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前日期字符串 (YYYY-MM-DD)
|
||||
*/
|
||||
const getCurrentDate = (): string => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理过期的限流历史记录(保留最近24小时)
|
||||
*/
|
||||
const cleanupOldHistory = (history: number[], currentTime: number): number[] => {
|
||||
const oneDayAgo = currentTime - (24 * 60 * 60 * 1000);
|
||||
return history.filter(timestamp => timestamp > oneDayAgo);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查客户端是否可以添加任务
|
||||
* @param clientId 客户端标识(IP地址或唯一ID)
|
||||
* @returns { canAdd: boolean, reason?: string, retryAfter?: number }
|
||||
*/
|
||||
export const checkRateLimit = (clientId: string): {
|
||||
canAdd: boolean;
|
||||
reason?: string;
|
||||
retryAfter?: number;
|
||||
dailyRemaining?: number;
|
||||
} => {
|
||||
const rateLimits = getRateLimits();
|
||||
const currentTime = Date.now();
|
||||
const currentDate = getCurrentDate();
|
||||
|
||||
// 获取或初始化客户端数据
|
||||
let clientData = rateLimits[clientId];
|
||||
if (!clientData) {
|
||||
clientData = {
|
||||
lastAddTime: 0,
|
||||
dailyCount: 0,
|
||||
lastResetDate: currentDate,
|
||||
history: [],
|
||||
};
|
||||
rateLimits[clientId] = clientData;
|
||||
}
|
||||
|
||||
// 检查日期是否变更,如果是则重置计数
|
||||
if (clientData.lastResetDate !== currentDate) {
|
||||
clientData.dailyCount = 0;
|
||||
clientData.lastResetDate = currentDate;
|
||||
clientData.history = [];
|
||||
}
|
||||
|
||||
// 清理过期的历史记录
|
||||
clientData.history = cleanupOldHistory(clientData.history, currentTime);
|
||||
|
||||
// 检查1:时间间隔限制(10分钟)
|
||||
const timeSinceLastAdd = currentTime - clientData.lastAddTime;
|
||||
if (timeSinceLastAdd < RATE_LIMIT_CONFIG.INTERVAL_MS) {
|
||||
const retryAfter = Math.ceil((RATE_LIMIT_CONFIG.INTERVAL_MS - timeSinceLastAdd) / 1000); // 秒
|
||||
return {
|
||||
canAdd: false,
|
||||
reason: 'RATE_LIMIT_INTERVAL',
|
||||
retryAfter,
|
||||
dailyRemaining: RATE_LIMIT_CONFIG.DAILY_MAX - clientData.dailyCount,
|
||||
};
|
||||
}
|
||||
|
||||
// 检查2:每日限制(6个任务)
|
||||
if (clientData.dailyCount >= RATE_LIMIT_CONFIG.DAILY_MAX) {
|
||||
return {
|
||||
canAdd: false,
|
||||
reason: 'RATE_LIMIT_DAILY',
|
||||
dailyRemaining: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 可以添加
|
||||
return {
|
||||
canAdd: true,
|
||||
dailyRemaining: RATE_LIMIT_CONFIG.DAILY_MAX - clientData.dailyCount - 1,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 记录客户端添加任务
|
||||
* @param clientId 客户端标识
|
||||
*/
|
||||
export const recordTaskAdd = (clientId: string): void => {
|
||||
const rateLimits = getRateLimits();
|
||||
const currentTime = Date.now();
|
||||
const currentDate = getCurrentDate();
|
||||
|
||||
let clientData = rateLimits[clientId];
|
||||
if (!clientData) {
|
||||
clientData = {
|
||||
lastAddTime: 0,
|
||||
dailyCount: 0,
|
||||
lastResetDate: currentDate,
|
||||
history: [],
|
||||
};
|
||||
}
|
||||
|
||||
clientData.lastAddTime = currentTime;
|
||||
clientData.dailyCount += 1;
|
||||
clientData.history.push(currentTime);
|
||||
clientData.history = cleanupOldHistory(clientData.history, currentTime);
|
||||
|
||||
rateLimits[clientId] = clientData;
|
||||
saveRateLimits(rateLimits);
|
||||
|
||||
console.log(`[Storage] Recorded task add for client ${clientId}: ` +
|
||||
`dailyCount=${clientData.dailyCount}, ` +
|
||||
`dailyRemaining=${RATE_LIMIT_CONFIG.DAILY_MAX - clientData.dailyCount}`);
|
||||
};
|
||||
16
src/lib/timezones.ts
Normal file
16
src/lib/timezones.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const TIMEZONES = [
|
||||
{ value: 'UTC', label: 'UTC (UTC+0)' },
|
||||
{ value: 'America/New_York', label: 'New York (UTC-5)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Los Angeles (UTC-8)' },
|
||||
{ value: 'America/Chicago', label: 'Chicago (UTC-6)' },
|
||||
{ value: 'Europe/London', label: 'London (UTC+0)' },
|
||||
{ value: 'Europe/Paris', label: 'Paris (UTC+1)' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin (UTC+1)' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai (UTC+4)' },
|
||||
{ value: 'Asia/Shanghai', label: 'Shanghai (UTC+8)' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo (UTC+9)' },
|
||||
{ value: 'Asia/Hong_Kong', label: 'Hong Kong (UTC+8)' },
|
||||
{ value: 'Asia/Singapore', label: 'Singapore (UTC+8)' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney (UTC+11)' },
|
||||
{ value: 'Pacific/Auckland', label: 'Auckland (UTC+13)' },
|
||||
];
|
||||
187
src/lib/translations.ts
Normal file
187
src/lib/translations.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
export const translations = {
|
||||
zh: {
|
||||
// Common
|
||||
save: '保存',
|
||||
cancel: '取消',
|
||||
close: '关闭',
|
||||
|
||||
// Home
|
||||
addTask: '添加新任务',
|
||||
addTaskPlaceholder: '添加新任务...',
|
||||
all: '全部',
|
||||
allTasks: '全部任务',
|
||||
active: '进行中',
|
||||
completed: '已完成',
|
||||
noTasks: '暂无任务,开始记录你的灵感吧',
|
||||
created: '创建',
|
||||
completedAt: '完成',
|
||||
subtitle1: '极简任务管理',
|
||||
subtitle2: '高效生活方式',
|
||||
|
||||
// Settings
|
||||
settings: '设置',
|
||||
language: '语言',
|
||||
languageDesc: '选择界面语言',
|
||||
logoCustomization: 'Logo 自定义',
|
||||
logoCustomizationDesc: '自定义主页 Logo 文字',
|
||||
logoTextLabel: 'Logo 文字',
|
||||
logoTextPlaceholder: '输入自定义文字(如 STARK)',
|
||||
timezone: '时区',
|
||||
timezoneDesc: '选择显示时区',
|
||||
themeMode: '主题模式',
|
||||
themeModeDesc: '选择外观主题',
|
||||
themeLight: '浅色模式',
|
||||
themeDark: '深色模式',
|
||||
themeSystem: '跟随系统',
|
||||
|
||||
// Analytics
|
||||
insights: '数据分析',
|
||||
analyticsLoading: '正在加载分析数据...',
|
||||
aggregatingRecords: '正在汇总数据...',
|
||||
past7Days: '过去 7 天',
|
||||
pastMonth: '过去一个月',
|
||||
allTime: '所有时间',
|
||||
totalCreated: '总创建数',
|
||||
successRate: '完成率',
|
||||
dailyActivity: '每日动态',
|
||||
creationVsCompletion: '创建与完成对比',
|
||||
recentTaskTimeline: '近期任务时间轴',
|
||||
completionDuration: '任务完成耗时分析',
|
||||
took: '耗时约',
|
||||
hours: '小时',
|
||||
liveDataEngine: '实时数据引擎',
|
||||
trend: '趋势',
|
||||
createdLabel: '已创建',
|
||||
completedLabel: '已完成',
|
||||
|
||||
// Notifications & Status
|
||||
settingsSaved: '设置已保存',
|
||||
syncingTasks: '正在同步任务...',
|
||||
noActiveTasks: '暂无进行中的任务',
|
||||
noCompletedTasks: '暂无已完成的任务',
|
||||
pv: '访问量 (PV)',
|
||||
uv: '访客数 (UV)',
|
||||
|
||||
// Auth
|
||||
authRequired: '需要验证权限',
|
||||
authDescription: '请输入密码以修改任务',
|
||||
enterPassword: '请输入密码',
|
||||
passwordRequired: '请输入密码',
|
||||
invalidPassword: '密码错误',
|
||||
verify: '验证',
|
||||
verifying: '验证中...',
|
||||
authHint: '如忘记密码,请联系管理员',
|
||||
authSuccess: '验证成功',
|
||||
logout: '退出登录',
|
||||
authenticated: '已验证',
|
||||
notAuthenticated: '未验证',
|
||||
|
||||
// Groups & Priority
|
||||
groups: '分组',
|
||||
addGroup: '添加分组',
|
||||
deleteGroup: '删除分组',
|
||||
defaultGroup: '默认分组',
|
||||
selectGroup: '选择分组',
|
||||
priority: '优先级',
|
||||
p0: '高优先级 (P0)',
|
||||
p1: '中优先级 (P1)',
|
||||
p2: '低优先级 (P2)',
|
||||
manageGroups: '管理分组',
|
||||
newGroupName: '新分组名称',
|
||||
noPriority: '无优先级',
|
||||
},
|
||||
en: {
|
||||
// Common
|
||||
save: 'Save',
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
|
||||
// Home
|
||||
addTask: 'Add Task',
|
||||
addTaskPlaceholder: 'Add a new task...',
|
||||
all: 'All',
|
||||
allTasks: 'All Tasks',
|
||||
active: 'Active',
|
||||
completed: 'Completed',
|
||||
noTasks: 'No tasks yet. Start tracking your ideas!',
|
||||
created: 'Created',
|
||||
completedAt: 'Completed',
|
||||
subtitle1: 'Minimal Task Management',
|
||||
subtitle2: 'Efficient Lifestyle',
|
||||
|
||||
// Settings
|
||||
settings: 'Settings',
|
||||
language: 'Language',
|
||||
languageDesc: 'Choose interface language',
|
||||
logoCustomization: 'Logo Customization',
|
||||
logoCustomizationDesc: 'Customize homepage logo text',
|
||||
logoTextLabel: 'Logo Text',
|
||||
logoTextPlaceholder: 'Enter custom text (e.g., STARK)',
|
||||
timezone: 'Timezone',
|
||||
timezoneDesc: 'Select display timezone',
|
||||
themeMode: 'Theme Mode',
|
||||
themeModeDesc: 'Choose appearance theme',
|
||||
themeLight: 'Light Mode',
|
||||
themeDark: 'Dark Mode',
|
||||
themeSystem: 'Follow System',
|
||||
|
||||
// Analytics
|
||||
insights: 'Insights',
|
||||
analyticsLoading: 'Loading analytics data...',
|
||||
aggregatingRecords: 'Aggregating records...',
|
||||
past7Days: 'Past 7 Days',
|
||||
pastMonth: 'Past Month',
|
||||
allTime: 'All Time',
|
||||
totalCreated: 'Total Created',
|
||||
successRate: 'Success Rate',
|
||||
dailyActivity: 'Daily Activity',
|
||||
creationVsCompletion: 'Creation vs Completion',
|
||||
recentTaskTimeline: 'Recent Task Timeline',
|
||||
completionDuration: 'Completion Time Analysis',
|
||||
took: 'Took ~',
|
||||
hours: 'h',
|
||||
liveDataEngine: 'Live Data Engine',
|
||||
trend: 'Trend',
|
||||
createdLabel: 'Created',
|
||||
completedLabel: 'Completed',
|
||||
|
||||
// Notifications & Status
|
||||
settingsSaved: 'Settings Saved',
|
||||
syncingTasks: 'Syncing tasks...',
|
||||
noActiveTasks: 'No active tasks',
|
||||
noCompletedTasks: 'No completed tasks',
|
||||
pv: 'Page Views (PV)',
|
||||
uv: 'Unique Visitors (UV)',
|
||||
|
||||
// Auth
|
||||
authRequired: 'Authentication Required',
|
||||
authDescription: 'Enter password to modify tasks',
|
||||
enterPassword: 'Enter password',
|
||||
passwordRequired: 'Password is required',
|
||||
invalidPassword: 'Invalid password',
|
||||
verify: 'Verify',
|
||||
verifying: 'Verifying...',
|
||||
authHint: 'Contact administrator if you forgot the password',
|
||||
authSuccess: 'Verified successfully',
|
||||
logout: 'Logout',
|
||||
authenticated: 'Authenticated',
|
||||
notAuthenticated: 'Not authenticated',
|
||||
|
||||
// Groups & Priority
|
||||
groups: 'Groups',
|
||||
addGroup: 'Add Group',
|
||||
deleteGroup: 'Delete Group',
|
||||
defaultGroup: 'Default Group',
|
||||
selectGroup: 'Select Group',
|
||||
priority: 'Priority',
|
||||
p0: 'High (P0)',
|
||||
p1: 'Medium (P1)',
|
||||
p2: 'Low (P2)',
|
||||
manageGroups: 'Manage Groups',
|
||||
newGroupName: 'New Group Name',
|
||||
noPriority: 'No Priority',
|
||||
},
|
||||
};
|
||||
|
||||
export type Language = keyof typeof translations;
|
||||
export type TranslationKey = keyof typeof translations.zh;
|
||||
26
src/lib/types.ts
Normal file
26
src/lib/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type Priority = 'P0' | 'P1' | 'P2';
|
||||
|
||||
export interface Todo {
|
||||
id: string;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
createdAt: number;
|
||||
completedAt?: number;
|
||||
deleted?: boolean;
|
||||
deletedAt?: number;
|
||||
groupId?: string;
|
||||
priority?: Priority;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_GROUP_ID = 'default';
|
||||
|
||||
export interface Stats {
|
||||
pv: number;
|
||||
uv: number;
|
||||
}
|
||||
Reference in New Issue
Block a user