feat: add growth tracking page and related functionality
- Implemented a new Growth page to track practice time and trends. - Added API integration for fetching review data. - Created components for displaying practice statistics and trends. - Updated navigation titles for the main index and growth pages. - Removed unused styles from the index page. - Introduced a Projects management page for adding and editing practice projects. - Developed a Record form for logging practice sessions with validation. - Added utility functions for date manipulation and duration formatting. - Implemented error handling and session management in the practice service. - Created unit tests for the practice service to ensure reliability.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
CREATE TABLE users (
|
||||
id text PRIMARY KEY,
|
||||
wechat_identity text NOT NULL UNIQUE,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE sessions (
|
||||
token_hash text PRIMARY KEY,
|
||||
user_id text NOT NULL REFERENCES users(id),
|
||||
expires_at timestamptz NOT NULL
|
||||
);
|
||||
CREATE INDEX sessions_expiry ON sessions(expires_at);
|
||||
|
||||
CREATE TABLE projects (
|
||||
id text PRIMARY KEY,
|
||||
user_id text NOT NULL REFERENCES users(id),
|
||||
name text NOT NULL CHECK (char_length(btrim(name)) BETWEEN 1 AND 40),
|
||||
archived boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
position bigint GENERATED ALWAYS AS IDENTITY,
|
||||
UNIQUE (user_id, id)
|
||||
);
|
||||
CREATE INDEX projects_owner ON projects(user_id, position);
|
||||
|
||||
CREATE TABLE records (
|
||||
id text PRIMARY KEY,
|
||||
user_id text NOT NULL REFERENCES users(id),
|
||||
project_id text NOT NULL,
|
||||
practice_date date NOT NULL CHECK (practice_date >= DATE '0001-01-01'),
|
||||
minutes integer NOT NULL CHECK (minutes > 0),
|
||||
note text NOT NULL DEFAULT '' CHECK (char_length(note) <= 2000),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
FOREIGN KEY (user_id, project_id) REFERENCES projects(user_id, id) ON DELETE RESTRICT
|
||||
);
|
||||
CREATE INDEX records_owner_date ON records(user_id, practice_date DESC, id DESC);
|
||||
CREATE INDEX records_owner_updated ON records(user_id, updated_at DESC, id DESC);
|
||||
CREATE INDEX records_project ON records(user_id, project_id);
|
||||
|
||||
-- Keep submission receipts after a record is deleted so a delayed retry cannot resurrect it.
|
||||
CREATE TABLE submissions (
|
||||
user_id text NOT NULL REFERENCES users(id),
|
||||
request_id text NOT NULL,
|
||||
payload_hash text NOT NULL,
|
||||
record_id text NOT NULL,
|
||||
PRIMARY KEY (user_id, request_id)
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed 001_practice.sql
|
||||
var practiceSQL string
|
||||
|
||||
// Migrate applies versioned migrations atomically. A transaction-scoped lock keeps
|
||||
// concurrent service starts from applying the same version twice.
|
||||
func Migrate(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err = tx.Exec(ctx, "SELECT pg_advisory_xact_lock(684127930)"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, "CREATE TABLE IF NOT EXISTS schema_migrations (version integer PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())"); err != nil {
|
||||
return err
|
||||
}
|
||||
var applied bool
|
||||
if err = tx.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=1)").Scan(&applied); err != nil {
|
||||
return err
|
||||
}
|
||||
if !applied {
|
||||
if _, err = tx.Exec(ctx, practiceSQL); err != nil {
|
||||
return fmt.Errorf("migration 1: %w", err)
|
||||
}
|
||||
if _, err = tx.Exec(ctx, "INSERT INTO schema_migrations(version) VALUES (1)"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user