Stage 8: UI social/account/history surfaces
Wire the deferred Stage 7 surfaces end-to-end (UI -> gateway transcode -> backend REST -> existing domain services): friends (incl. one-time friend codes), per-user blocks, friend-game invitations, profile editing + email binding, the statistics screen, and the in-game history + GCG export. Friends gain two add paths (interview decision, a deliberate plan change): one-time 6-digit codes (friend_codes table, 12h TTL, single-use, rate-limited redeem); and play-gated requests (shared game required) where an explicit decline is permanent, an ignored request lapses after 30 days, and a code bypasses a decline. Migration 00006 widens friendships_status_chk and adds friend_codes. Lobby notification badge is poll + push: a new generic `notify` event drives it live; the client polls on open/focus. Language stays a single Settings control that writes through to the durable account's preferred_language. GCG export is finished-only (game.ErrGameActive) and shares/downloads the .gcg file. Tests: backend unit + inttest (friend gate/decline/code, ListInvitations, GetStats, GCG gate), gateway transcode round-trips + notify constructor, UI vitest (codecs, win-rate, share choice) + Playwright social specs. Docs: PLAN (Stage 8 done + refinements + TODO-5), ARCHITECTURE, FUNCTIONAL(+ru), UI_DESIGN, TESTING, module READMEs.
This commit is contained in:
@@ -35,6 +35,12 @@ func (s *Server) registerRoutes() {
|
||||
u := s.user
|
||||
if s.accounts != nil {
|
||||
u.GET("/profile", s.handleProfile)
|
||||
u.PUT("/profile", s.handleUpdateProfile)
|
||||
u.GET("/stats", s.handleStats)
|
||||
}
|
||||
if s.emails != nil {
|
||||
u.POST("/email/request", s.handleEmailBindRequest)
|
||||
u.POST("/email/confirm", s.handleEmailBindConfirm)
|
||||
}
|
||||
if s.games != nil {
|
||||
u.GET("/games", s.handleListGames)
|
||||
@@ -48,15 +54,34 @@ func (s *Server) registerRoutes() {
|
||||
u.GET("/games/:id/check_word", s.handleCheckWord)
|
||||
u.POST("/games/:id/complaint", s.handleComplaint)
|
||||
u.GET("/games/:id/history", s.handleHistory)
|
||||
u.GET("/games/:id/gcg", s.handleExportGCG)
|
||||
}
|
||||
if s.matchmaker != nil {
|
||||
u.POST("/lobby/enqueue", s.handleEnqueue)
|
||||
u.GET("/lobby/poll", s.handlePoll)
|
||||
}
|
||||
if s.invitations != nil {
|
||||
u.GET("/invitations", s.handleListInvitations)
|
||||
u.POST("/invitations", s.handleCreateInvitation)
|
||||
u.POST("/invitations/:id/accept", s.handleAcceptInvitation)
|
||||
u.POST("/invitations/:id/decline", s.handleDeclineInvitation)
|
||||
u.DELETE("/invitations/:id", s.handleCancelInvitation)
|
||||
}
|
||||
if s.social != nil {
|
||||
u.POST("/games/:id/chat", s.handleChatPost)
|
||||
u.GET("/games/:id/chat", s.handleChatList)
|
||||
u.POST("/games/:id/nudge", s.handleNudge)
|
||||
u.GET("/friends", s.handleListFriends)
|
||||
u.GET("/friends/incoming", s.handleIncomingRequests)
|
||||
u.POST("/friends/request", s.handleFriendRequest)
|
||||
u.POST("/friends/respond", s.handleFriendRespond)
|
||||
u.POST("/friends/cancel", s.handleFriendCancel)
|
||||
u.DELETE("/friends/:id", s.handleUnfriend)
|
||||
u.POST("/friends/code", s.handleIssueFriendCode)
|
||||
u.POST("/friends/code/redeem", s.handleRedeemFriendCode)
|
||||
u.GET("/blocks", s.handleListBlocks)
|
||||
u.POST("/blocks", s.handleBlock)
|
||||
u.DELETE("/blocks/:id", s.handleUnblock)
|
||||
}
|
||||
s.admin.GET("/ping", s.handleAdminPing)
|
||||
}
|
||||
@@ -117,8 +142,30 @@ func statusForError(err error) (int, string) {
|
||||
return http.StatusConflict, "not_your_turn"
|
||||
case errors.Is(err, game.ErrFinished), errors.Is(err, social.ErrGameNotActive):
|
||||
return http.StatusConflict, "game_finished"
|
||||
case errors.Is(err, game.ErrGameActive):
|
||||
return http.StatusConflict, "game_active"
|
||||
case errors.Is(err, account.ErrInvalidProfile):
|
||||
return http.StatusBadRequest, "invalid_profile"
|
||||
case errors.Is(err, account.ErrAlreadyConfirmed):
|
||||
return http.StatusConflict, "already_confirmed"
|
||||
case errors.Is(err, lobby.ErrAlreadyQueued):
|
||||
return http.StatusConflict, "already_queued"
|
||||
case errors.Is(err, lobby.ErrInvalidInvitation):
|
||||
return http.StatusBadRequest, "invalid_invitation"
|
||||
case errors.Is(err, lobby.ErrInvitationBlocked):
|
||||
return http.StatusForbidden, "invitation_blocked"
|
||||
case errors.Is(err, lobby.ErrInvitationNotFound):
|
||||
return http.StatusNotFound, "invitation_not_found"
|
||||
case errors.Is(err, lobby.ErrInvitationNotPending):
|
||||
return http.StatusConflict, "invitation_not_pending"
|
||||
case errors.Is(err, lobby.ErrInvitationExpired):
|
||||
return http.StatusConflict, "invitation_expired"
|
||||
case errors.Is(err, lobby.ErrNotInvited):
|
||||
return http.StatusForbidden, "not_invited"
|
||||
case errors.Is(err, lobby.ErrAlreadyResponded):
|
||||
return http.StatusConflict, "already_responded"
|
||||
case errors.Is(err, lobby.ErrNotInviter):
|
||||
return http.StatusForbidden, "not_inviter"
|
||||
case errors.Is(err, game.ErrInvalidConfig):
|
||||
return http.StatusBadRequest, "invalid_config"
|
||||
case errors.Is(err, game.ErrNoHintAvailable):
|
||||
@@ -142,6 +189,20 @@ func statusForError(err error) (int, string) {
|
||||
errors.Is(err, social.ErrEmptyMessage), errors.Is(err, social.ErrForbiddenContent),
|
||||
errors.Is(err, social.ErrNudgeTooSoon):
|
||||
return http.StatusUnprocessableEntity, "chat_rejected"
|
||||
case errors.Is(err, social.ErrSelfRelation):
|
||||
return http.StatusBadRequest, "self_relation"
|
||||
case errors.Is(err, social.ErrRequestExists):
|
||||
return http.StatusConflict, "request_exists"
|
||||
case errors.Is(err, social.ErrRequestBlocked):
|
||||
return http.StatusForbidden, "request_blocked"
|
||||
case errors.Is(err, social.ErrRequestNotFound):
|
||||
return http.StatusNotFound, "request_not_found"
|
||||
case errors.Is(err, social.ErrNoSharedGame):
|
||||
return http.StatusForbidden, "no_shared_game"
|
||||
case errors.Is(err, social.ErrRequestDeclined):
|
||||
return http.StatusConflict, "request_declined"
|
||||
case errors.Is(err, social.ErrFriendCodeInvalid):
|
||||
return http.StatusUnprocessableEntity, "friend_code_invalid"
|
||||
default:
|
||||
return http.StatusInternalServerError, "internal"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user