feat: add project icon picker component and integrate with project management
- Implemented ProjectIconPicker component for selecting project icons. - Created ProjectIcon component to display selected icons. - Updated calendar page to remove unused PageHeading component. - Modified growth page to enhance UI and improve text clarity. - Refactored index page to include project icons and improve layout. - Added mine page for user profile management with avatar selection. - Introduced projects-sort page for managing project order with drag-and-drop functionality. - Enhanced projects page to allow icon selection when adding or editing projects. - Updated record page to streamline user experience and improve form actions. - Adjusted practice service to support project icon management. - Added tests for project saving functionality to ensure correct handling of icons. - Updated global type definitions to support SVG and PNG imports.
This commit is contained in:
@@ -27,7 +27,10 @@ var (
|
||||
ErrDeleted = errors.New("这次提交的记录已被删除,不会重复创建")
|
||||
)
|
||||
|
||||
var presets = []string{"零基础", "基础提升", "软开素质", "足髋训练", "核心臀腿", "小球核心", "天鹅臂颈", "呼吸训练"}
|
||||
var presets = []struct{ name, iconID string }{
|
||||
{"零基础", "plie"}, {"基础提升", "tendu"}, {"软开素质", "flexibility"}, {"足髋训练", "foot-hip"},
|
||||
{"核心臀腿", "core-glutes"}, {"小球核心", "ball-core"}, {"天鹅臂颈", "swan-arms"}, {"呼吸训练", "breathing"},
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
db *pgxpool.Pool
|
||||
@@ -73,8 +76,8 @@ func (s *Store) Login(ctx context.Context, identity string) (Session, error) {
|
||||
return Session{}, err
|
||||
}
|
||||
if result.RowsAffected() == 1 {
|
||||
for _, name := range presets {
|
||||
if _, err = tx.Exec(ctx, "INSERT INTO projects(id,user_id,name) VALUES($1,$2,$3)", newID(), id, name); err != nil {
|
||||
for _, preset := range presets {
|
||||
if _, err = tx.Exec(ctx, "INSERT INTO projects(id,user_id,name,icon_id) VALUES($1,$2,$3,$4)", newID(), id, preset.name, preset.iconID); err != nil {
|
||||
return Session{}, err
|
||||
}
|
||||
}
|
||||
@@ -133,12 +136,13 @@ func (s *Store) write(ctx context.Context, user string, fn func(pgx.Tx) error) e
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IconID string `json:"iconId"`
|
||||
Archived bool `json:"archived"`
|
||||
HasRecords bool `json:"hasRecords"`
|
||||
}
|
||||
|
||||
func (s *Store) Projects(ctx context.Context, user string, archived bool) ([]Project, error) {
|
||||
rows, err := s.db.Query(ctx, `SELECT p.id,p.name,p.archived,EXISTS(SELECT 1 FROM records r WHERE r.user_id=p.user_id AND r.project_id=p.id)
|
||||
rows, err := s.db.Query(ctx, `SELECT p.id,p.name,p.icon_id,p.archived,EXISTS(SELECT 1 FROM records r WHERE r.user_id=p.user_id AND r.project_id=p.id)
|
||||
FROM projects p WHERE p.user_id=$1 AND ($2 OR NOT p.archived) ORDER BY p.position`, user, archived)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -147,7 +151,7 @@ func (s *Store) Projects(ctx context.Context, user string, archived bool) ([]Pro
|
||||
projects := []Project{}
|
||||
for rows.Next() {
|
||||
var p Project
|
||||
if err := rows.Scan(&p.ID, &p.Name, &p.Archived, &p.HasRecords); err != nil {
|
||||
if err := rows.Scan(&p.ID, &p.Name, &p.IconID, &p.Archived, &p.HasRecords); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projects = append(projects, p)
|
||||
@@ -155,20 +159,75 @@ func (s *Store) Projects(ctx context.Context, user string, archived bool) ([]Pro
|
||||
return projects, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) SaveProject(ctx context.Context, user, id, name string) (Project, error) {
|
||||
// ReorderProjects accepts exactly the owner's active project IDs. Reusing their
|
||||
// current position slots leaves archived history and automatic append positions intact.
|
||||
func (s *Store) ReorderProjects(ctx context.Context, user string, ids []string) error {
|
||||
if ids == nil {
|
||||
return ErrInvalid
|
||||
}
|
||||
return s.write(ctx, user, func(tx pgx.Tx) error {
|
||||
rows, err := tx.Query(ctx, `SELECT id,position FROM projects WHERE user_id=$1 AND NOT archived ORDER BY position`, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
positions := make([]int64, 0, len(ids))
|
||||
available := make(map[string]bool, len(ids))
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var position int64
|
||||
if err := rows.Scan(&id, &position); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
positions = append(positions, position)
|
||||
available[id] = true
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ids) != len(positions) {
|
||||
return ErrInvalid
|
||||
}
|
||||
for _, id := range ids {
|
||||
if !available[id] {
|
||||
return ErrInvalid
|
||||
}
|
||||
delete(available, id)
|
||||
}
|
||||
for index, id := range ids {
|
||||
if _, err := tx.Exec(ctx, `UPDATE projects SET position=$3 WHERE user_id=$1 AND id=$2 AND NOT archived`, user, id, positions[index]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// SaveProject persists a name and optional icon choice within the owner's account.
|
||||
// A nil iconID preserves existing artwork for older clients; an empty string clears it.
|
||||
// Unknown icon IDs return ErrInvalid, and another owner's project returns ErrUnavailable.
|
||||
func (s *Store) SaveProject(ctx context.Context, user, id, name string, iconID *string) (Project, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || utf8.RuneCountInString(name) > NameLimit || strings.ContainsRune(name, 0) {
|
||||
return Project{}, ErrInvalid
|
||||
}
|
||||
if iconID != nil && !validProjectIcon(*iconID) {
|
||||
return Project{}, ErrInvalid
|
||||
}
|
||||
p := Project{ID: id, Name: name}
|
||||
if iconID != nil {
|
||||
p.IconID = *iconID
|
||||
}
|
||||
err := s.write(ctx, user, func(tx pgx.Tx) error {
|
||||
if id == "" {
|
||||
p.ID = newID()
|
||||
_, err := tx.Exec(ctx, "INSERT INTO projects(id,user_id,name) VALUES($1,$2,$3)", p.ID, user, name)
|
||||
_, err := tx.Exec(ctx, "INSERT INTO projects(id,user_id,name,icon_id) VALUES($1,$2,$3,$4)", p.ID, user, name, p.IconID)
|
||||
return err
|
||||
}
|
||||
err := tx.QueryRow(ctx, `UPDATE projects SET name=$3 WHERE user_id=$1 AND id=$2 RETURNING archived,
|
||||
EXISTS(SELECT 1 FROM records WHERE user_id=$1 AND project_id=$2)`, user, id, name).Scan(&p.Archived, &p.HasRecords)
|
||||
err := tx.QueryRow(ctx, `UPDATE projects SET name=$3,icon_id=COALESCE($4,icon_id) WHERE user_id=$1 AND id=$2 RETURNING icon_id,archived,
|
||||
EXISTS(SELECT 1 FROM records WHERE user_id=$1 AND project_id=$2)`, user, id, name, iconID).Scan(&p.IconID, &p.Archived, &p.HasRecords)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrUnavailable
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user