Add multi-theme system with random theme selection on app startup
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# private key and sensitive files
|
||||
keystore.properties
|
||||
upload-keystore.properties
|
||||
upload-keystore.jks
|
||||
*.jks
|
||||
*.pem
|
||||
*.p12
|
||||
*.pfx
|
||||
.env*
|
||||
*.key
|
||||
|
||||
# Tauri build artifacts and generated files
|
||||
src-tauri/gen/
|
||||
src-tauri/target/
|
||||
src-tauri/.gradle/
|
||||
*.apk
|
||||
*.aab
|
48
CLAUDE.md
Normal file
@ -0,0 +1,48 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Frontend Development
|
||||
- `pnpm dev` - Start the frontend development server (runs Vite on localhost:1420)
|
||||
- `pnpm build` - Build the frontend for production (TypeScript compilation + Vite build)
|
||||
- `pnpm preview` - Preview the production build
|
||||
|
||||
### Tauri Development
|
||||
- `pnpm tauri dev` - Start Tauri development mode (builds Rust backend + starts frontend)
|
||||
- `pnpm tauri build` - Build the complete Tauri application for production
|
||||
|
||||
### Build Scripts
|
||||
- `./build_desktop.sh` - Build desktop application
|
||||
- `./build_android.sh` - Build Android application
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This is a **Tauri application** combining a Rust backend with a React/TypeScript frontend for building cross-platform desktop and mobile apps.
|
||||
|
||||
### Backend (Rust)
|
||||
- **Database**: SQLite with rusqlite for storing anniversary records
|
||||
- **Location**: Database stored in app data directory (`anniversaries.db`)
|
||||
- **Schema**: `anniversaries` table with id, title, start_date, created_at
|
||||
- **Core Commands**: All database operations exposed as Tauri commands in `src-tauri/src/lib.rs`
|
||||
- `get_anniversaries` - Fetch all anniversaries with calculated days
|
||||
- `get_anniversary_by_id` - Fetch single anniversary by ID
|
||||
- `add_anniversary` - Add new anniversary
|
||||
- `delete_anniversary` - Delete anniversary by ID
|
||||
|
||||
### Frontend (React + TypeScript)
|
||||
- **Router**: React Router DOM with three main routes:
|
||||
- `/` - HomePage (list all anniversaries)
|
||||
- `/detail/:id` - DetailPage (view/delete specific anniversary)
|
||||
- `/add` - AddPage (create new anniversary)
|
||||
- **Data Flow**: Frontend calls Tauri commands to interact with Rust backend
|
||||
- **Styling**: Component-specific CSS files for each page
|
||||
|
||||
### Key Dependencies
|
||||
- **Frontend**: React 18, React Router DOM, Tauri API
|
||||
- **Backend**: rusqlite (SQLite), chrono (date handling), serde (serialization)
|
||||
- **Build**: Vite, TypeScript, Tauri CLI
|
||||
|
||||
### Package Manager
|
||||
This project uses **pnpm** as specified in `tauri.conf.json` build commands.
|
7
README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Tauri + React + Typescript
|
||||
|
||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
1
build_android.sh
Executable file
@ -0,0 +1 @@
|
||||
pnpm tauri android build --apk -t aarch64
|
1
build_desktop.sh
Executable file
@ -0,0 +1 @@
|
||||
pnpm tauri build --no-bundle
|
14
index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + Typescript</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "countdown",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "cargo tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
1239
pnpm-lock.yaml
generated
Normal file
7
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
5101
src-tauri/Cargo.lock
generated
Normal file
26
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "countdown"
|
||||
version = "0.1.0"
|
||||
description = "A Countdown app"
|
||||
authors = ["sangge"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "countdown_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { version = "0.33.0", features = ["bundled"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
chrono = "0.4.39"
|
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
10
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 482 B |
BIN
src-tauri/icons/4096x4096.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 447 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 509 B |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 755 B |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
174
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use chrono::{Local, NaiveDate};
|
||||
use rusqlite::{Connection, Result};
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
|
||||
fn init_database(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 获取应用数据目录
|
||||
let app_dir = app.path().app_data_dir()?;
|
||||
|
||||
// 确保目录存在
|
||||
fs::create_dir_all(&app_dir)?;
|
||||
|
||||
// 构建数据库文件路径
|
||||
let db_path = app_dir.join("anniversaries.db");
|
||||
|
||||
// 打开数据库连接
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
// 创建表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS anniversaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
start_date TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Anniversary {
|
||||
id: i32,
|
||||
title: String,
|
||||
start_date: String,
|
||||
days: i64,
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn get_anniversaries(app_handle: tauri::AppHandle) -> Result<Vec<Anniversary>, String> {
|
||||
// 返回主页数据
|
||||
let app_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let db_path = app_dir.join("anniversaries.db");
|
||||
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, title, start_date FROM anniversaries ORDER BY created_at DESC")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
|
||||
let anniversaries = stmt
|
||||
.query_map([], |row| {
|
||||
let id: i32 = row.get(0)?;
|
||||
let title: String = row.get(1)?;
|
||||
let start_date: String = row.get(2)?;
|
||||
|
||||
// 计算天数
|
||||
let start = NaiveDate::parse_from_str(&start_date, "%Y-%m-%d")
|
||||
.map_err(|e| rusqlite::Error::InvalidParameterName(e.to_string()))?;
|
||||
let days = (today - start).num_days();
|
||||
|
||||
Ok(Anniversary {
|
||||
id,
|
||||
title,
|
||||
start_date,
|
||||
days,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let result: Result<Vec<_>, _> = anniversaries.collect();
|
||||
result.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_anniversary_by_id(
|
||||
app_handle: tauri::AppHandle,
|
||||
id: i32,
|
||||
) -> Result<Anniversary, String> {
|
||||
// 返回详情页数据
|
||||
let app_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let db_path = app_dir.join("anniversaries.db");
|
||||
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT title, start_date FROM anniversaries WHERE id = ?")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
|
||||
let result = stmt
|
||||
.query_row([id], |row| {
|
||||
let title: String = row.get(0)?;
|
||||
let start_date: String = row.get(1)?;
|
||||
|
||||
// 计算天数
|
||||
let start = NaiveDate::parse_from_str(&start_date, "%Y-%m-%d")
|
||||
.map_err(|e| rusqlite::Error::InvalidParameterName(e.to_string()))?;
|
||||
let days = (today - start).num_days();
|
||||
|
||||
Ok(Anniversary {
|
||||
id,
|
||||
title,
|
||||
start_date,
|
||||
days,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_anniversary(app_handle: tauri::AppHandle, id: i64) -> Result<(), String> {
|
||||
// 详情页删除
|
||||
let app_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let db_path = app_dir.join("anniversaries.db");
|
||||
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||
conn.execute("DELETE FROM anniversaries WHERE id = ?", [id])
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn add_anniversary(
|
||||
app_handle: tauri::AppHandle,
|
||||
title: String,
|
||||
start_date: String,
|
||||
) -> Result<(), String> {
|
||||
// 主页添加
|
||||
let app_dir = app_handle
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let db_path = app_dir.join("anniversaries.db");
|
||||
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"INSERT INTO anniversaries (title, start_date) VALUES (?1, ?2)",
|
||||
[&title, &start_date],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
init_database(app).expect("failed to initialize database");
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_anniversaries,
|
||||
get_anniversary_by_id,
|
||||
add_anniversary,
|
||||
delete_anniversary,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
6
src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
countdown_lib::run()
|
||||
}
|
36
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "countdown",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.countdown.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "countdown",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "rpm", "appimage", "nsis", "msi", "dmg", "app"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
222
src/AddPage.css
Normal file
@ -0,0 +1,222 @@
|
||||
/* 基础样式 */
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
background: var(--bg-secondary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: var(--bg-card);
|
||||
padding: 32px;
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.2s ease;
|
||||
outline: none;
|
||||
background: var(--bg-card);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background: var(--color-primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.submit-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 桌面端样式 */
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端样式 */
|
||||
@media screen and (max-width: 767px) and (min-width: 481px) {
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端样式 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 特小屏幕样式 */
|
||||
@media screen and (max-width: 320px) {
|
||||
.container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保所有元素使用 border-box */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 优化输入控件在移动端的表现 */
|
||||
@media (hover: none) {
|
||||
input {
|
||||
font-size: 16px !important; /* 防止 iOS 自动缩放 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 针对暗色模式的优化 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.form-card {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
}
|
59
src/AddPage.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./AddPage.css";
|
||||
|
||||
function AddPage() {
|
||||
const navigate = useNavigate();
|
||||
const [title, setTitle] = useState("");
|
||||
const [date, setDate] = useState("");
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await invoke("add_anniversary", {
|
||||
title,
|
||||
startDate: date,
|
||||
});
|
||||
// 添加成功后返回主页
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
console.error("Failed to add anniversary:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="nav-bar">
|
||||
<button className="back-button" onClick={() => navigate("/")}>
|
||||
<span className="back-icon">←</span>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<h1 className="page-title">Add New</h1>
|
||||
</div>
|
||||
<div className="form-card">
|
||||
<div className="form-group">
|
||||
<label>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Enter anniversary title"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="submit-button" onClick={handleSubmit}>
|
||||
Create Anniversary
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddPage;
|
1
src/App.css
Normal file
@ -0,0 +1 @@
|
||||
/* App.css */
|
36
src/App.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import HomePage from "./HomePage";
|
||||
import DetailPage from "./DetailPage";
|
||||
import AddPage from "./AddPage";
|
||||
import "./themes.css";
|
||||
|
||||
const THEMES = [
|
||||
"blue",
|
||||
"green",
|
||||
"purple",
|
||||
"orange",
|
||||
"pink",
|
||||
"cyan",
|
||||
"dark",
|
||||
"gradient"
|
||||
];
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)];
|
||||
document.documentElement.setAttribute('data-theme', randomTheme);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/detail/:id" element={<DetailPage />} />
|
||||
<Route path="/add" element={<AddPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
79
src/DetailPage.css
Normal file
@ -0,0 +1,79 @@
|
||||
.container {
|
||||
padding: 24px;
|
||||
background: var(--bg-secondary);
|
||||
min-height: 100vh;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-primary);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background-color: var(--color-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
background-color: var(--color-danger-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(255, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 24px 0;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.days {
|
||||
font-size: 64px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
margin: 32px 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.start-date {
|
||||
font-size: 16px;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 16px;
|
||||
}
|
66
src/DetailPage.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./DetailPage.css";
|
||||
|
||||
interface Anniversary {
|
||||
id: number;
|
||||
title: string;
|
||||
start_date: string;
|
||||
days: number;
|
||||
}
|
||||
|
||||
function DetailPage() {
|
||||
const { id } = useParams(); // 获取路由参数
|
||||
const navigate = useNavigate(); // 获取导航函数
|
||||
const [anniversary, setAnniversary] = useState<Anniversary>({
|
||||
id: 0,
|
||||
title: "",
|
||||
start_date: "",
|
||||
days: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const getAnniversaryData = async () => {
|
||||
if (id) {
|
||||
const result = await invoke<Anniversary>("get_anniversary_by_id", {
|
||||
id: parseInt(id),
|
||||
});
|
||||
setAnniversary(result);
|
||||
}
|
||||
};
|
||||
getAnniversaryData();
|
||||
}, [id]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (id) {
|
||||
try {
|
||||
await invoke("delete_anniversary", { id: parseInt(id) });
|
||||
navigate("/"); // 删除成功后返回主页
|
||||
} catch (error) {
|
||||
console.error("Failed to delete anniversary:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="nav-bar">
|
||||
<button className="back-button" onClick={() => navigate("/")}>
|
||||
<span className="back-icon">←</span>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<button className="delete-button" onClick={handleDelete}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h1 className="title">{anniversary.title}</h1>
|
||||
<div className="days">{anniversary.days}</div>
|
||||
<div className="start-date">Start Date: {anniversary.start_date}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPage;
|
181
src/HomePage.css
Normal file
@ -0,0 +1,181 @@
|
||||
.container {
|
||||
padding: 16px;
|
||||
background: var(--bg-primary);
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
position: relative;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
background-color: var(--color-primary);
|
||||
border: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.anniversary-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.anniversary-card {
|
||||
background: var(--bg-card);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.anniversary-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.anniversary-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.anniversary-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.days-count {
|
||||
font-size: 20px;
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 桌面端样式 */
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.anniversary-list {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.anniversary-card {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端样式 */
|
||||
@media screen and (max-width: 767px) {
|
||||
.container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.anniversary-card {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.anniversary-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.days-count {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 针对特别窄的屏幕 */
|
||||
@media screen and (max-width: 320px) {
|
||||
.container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.anniversary-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.days-count {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保所有元素都使用 border-box */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
61
src/HomePage.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./HomePage.css";
|
||||
|
||||
interface Anniversary {
|
||||
id: number;
|
||||
title: string;
|
||||
start_date: string;
|
||||
days: number;
|
||||
}
|
||||
|
||||
function HomePage() {
|
||||
const [anniversaries, setAnniversaries] = useState<Anniversary[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAnniversaries = async () => {
|
||||
const result = await invoke<Anniversary[]>("get_anniversaries");
|
||||
setAnniversaries(result);
|
||||
};
|
||||
loadAnniversaries();
|
||||
}, []);
|
||||
// 获取导航函数
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 点击卡片时跳转到详情页
|
||||
const handleCardClick = (id: number) => {
|
||||
navigate(`/detail/${id}`); // 例如:/detail/1
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header-bar">
|
||||
<h1>Count Down</h1>
|
||||
<button className="add-button" onClick={() => navigate("/add")}>
|
||||
<span className="plus-icon">+</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="anniversary-list">
|
||||
{anniversaries.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="anniversary-card"
|
||||
onClick={() => {
|
||||
handleCardClick(item.id);
|
||||
}}
|
||||
>
|
||||
<div className="anniversary-title">{item.title}</div>
|
||||
<div className="anniversary-info">
|
||||
<span className="days-count">{item.days}</span>
|
||||
<span className="start-date">{item.start_date}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomePage;
|
9
src/main.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
153
src/themes.css
Normal file
@ -0,0 +1,153 @@
|
||||
/* 主题系统 - CSS变量定义 */
|
||||
|
||||
/* 默认主题(蓝色) */
|
||||
:root[data-theme="blue"] {
|
||||
--bg-primary: #f5f5f5;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--bg-card: #ffffff;
|
||||
--color-primary: #4a90e2;
|
||||
--color-primary-hover: #357abd;
|
||||
--color-text: #2c3e50;
|
||||
--color-text-secondary: #666;
|
||||
--color-text-muted: #6c757d;
|
||||
--color-border: #e9ecef;
|
||||
--color-danger: #ff4444;
|
||||
--color-danger-hover: #e60000;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-primary: 0 2px 8px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
/* 绿色主题 */
|
||||
:root[data-theme="green"] {
|
||||
--bg-primary: #f0f8f0;
|
||||
--bg-secondary: #f5fbf5;
|
||||
--bg-card: #ffffff;
|
||||
--color-primary: #28a745;
|
||||
--color-primary-hover: #218838;
|
||||
--color-text: #2d5016;
|
||||
--color-text-secondary: #555;
|
||||
--color-text-muted: #6c7b6c;
|
||||
--color-border: #d4edda;
|
||||
--color-danger: #dc3545;
|
||||
--color-danger-hover: #c82333;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-primary: 0 2px 8px rgba(40, 167, 69, 0.2);
|
||||
}
|
||||
|
||||
/* 紫色主题 */
|
||||
:root[data-theme="purple"] {
|
||||
--bg-primary: #f8f5ff;
|
||||
--bg-secondary: #faf7ff;
|
||||
--bg-card: #ffffff;
|
||||
--color-primary: #8b5cf6;
|
||||
--color-primary-hover: #7c3aed;
|
||||
--color-text: #4c1d95;
|
||||
--color-text-secondary: #666;
|
||||
--color-text-muted: #7c6c95;
|
||||
--color-border: #e9d5ff;
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-hover: #dc2626;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-primary: 0 2px 8px rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
/* 橙色主题 */
|
||||
:root[data-theme="orange"] {
|
||||
--bg-primary: #fff7ed;
|
||||
--bg-secondary: #fffbf5;
|
||||
--bg-card: #ffffff;
|
||||
--color-primary: #f97316;
|
||||
--color-primary-hover: #ea580c;
|
||||
--color-text: #9a3412;
|
||||
--color-text-secondary: #666;
|
||||
--color-text-muted: #a16c3c;
|
||||
--color-border: #fed7aa;
|
||||
--color-danger: #dc2626;
|
||||
--color-danger-hover: #b91c1c;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-primary: 0 2px 8px rgba(249, 115, 22, 0.2);
|
||||
}
|
||||
|
||||
/* 粉色主题 */
|
||||
:root[data-theme="pink"] {
|
||||
--bg-primary: #fdf2f8;
|
||||
--bg-secondary: #fef7f0;
|
||||
--bg-card: #ffffff;
|
||||
--color-primary: #ec4899;
|
||||
--color-primary-hover: #db2777;
|
||||
--color-text: #831843;
|
||||
--color-text-secondary: #666;
|
||||
--color-text-muted: #a15a7a;
|
||||
--color-border: #f9a8d4;
|
||||
--color-danger: #dc2626;
|
||||
--color-danger-hover: #b91c1c;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-primary: 0 2px 8px rgba(236, 72, 153, 0.2);
|
||||
}
|
||||
|
||||
/* 青色主题 */
|
||||
:root[data-theme="cyan"] {
|
||||
--bg-primary: #ecfdf5;
|
||||
--bg-secondary: #f0fdfa;
|
||||
--bg-card: #ffffff;
|
||||
--color-primary: #06b6d4;
|
||||
--color-primary-hover: #0891b2;
|
||||
--color-text: #164e63;
|
||||
--color-text-secondary: #666;
|
||||
--color-text-muted: #4c7a7a;
|
||||
--color-border: #a7f3d0;
|
||||
--color-danger: #dc2626;
|
||||
--color-danger-hover: #b91c1c;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
--shadow-primary: 0 2px 8px rgba(6, 182, 212, 0.2);
|
||||
}
|
||||
|
||||
/* 暗色主题 */
|
||||
:root[data-theme="dark"] {
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #111827;
|
||||
--bg-card: #374151;
|
||||
--color-primary: #60a5fa;
|
||||
--color-primary-hover: #3b82f6;
|
||||
--color-text: #f9fafb;
|
||||
--color-text-secondary: #d1d5db;
|
||||
--color-text-muted: #9ca3af;
|
||||
--color-border: #4b5563;
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-hover: #dc2626;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||
--shadow-primary: 0 2px 8px rgba(96, 165, 250, 0.3);
|
||||
}
|
||||
|
||||
/* 渐变主题 */
|
||||
:root[data-theme="gradient"] {
|
||||
--bg-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--bg-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
--bg-card: rgba(255, 255, 255, 0.95);
|
||||
--color-primary: #667eea;
|
||||
--color-primary-hover: #5a6fd8;
|
||||
--color-text: #2d3748;
|
||||
--color-text-secondary: #4a5568;
|
||||
--color-text-muted: #718096;
|
||||
--color-border: rgba(255, 255, 255, 0.3);
|
||||
--color-danger: #e53e3e;
|
||||
--color-danger-hover: #c53030;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
--shadow-primary: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
32
vite.config.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
}));
|