commit
f2713c4bd3
@ -0,0 +1,78 @@
|
||||
package online.wesal.wesal.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import online.wesal.wesal.dto.ApiResponse;
|
||||
import online.wesal.wesal.dto.DailyChallengeResponse;
|
||||
import online.wesal.wesal.dto.LeaderboardEntry;
|
||||
import online.wesal.wesal.dto.PuzzleAttemptRequest;
|
||||
import online.wesal.wesal.entity.Puzzle;
|
||||
import online.wesal.wesal.entity.PuzzleAttempt;
|
||||
import online.wesal.wesal.service.PuzzleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/puzzles")
|
||||
@CrossOrigin(origins = "*")
|
||||
@Tag(name = "Puzzles", description = "Wordle puzzle endpoints")
|
||||
public class PuzzleController {
|
||||
|
||||
@Autowired
|
||||
private PuzzleService puzzleService;
|
||||
|
||||
@GetMapping("/dailyChallenge")
|
||||
@Operation(summary = "Get daily challenge", description = "Returns today's Wordle word and whether user has solved it")
|
||||
public ResponseEntity<ApiResponse<DailyChallengeResponse>> getDailyChallenge() {
|
||||
try {
|
||||
Puzzle todaysPuzzle = puzzleService.getTodaysPuzzle();
|
||||
boolean solved = puzzleService.hasUserSolvedToday();
|
||||
|
||||
DailyChallengeResponse response = new DailyChallengeResponse(todaysPuzzle.getWord(), solved);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.ok(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/attempt")
|
||||
@Operation(summary = "Submit puzzle attempt", description = "Submit user's attempt for today's puzzle")
|
||||
public ResponseEntity<ApiResponse<String>> submitAttempt(@Valid @RequestBody PuzzleAttemptRequest request) {
|
||||
try {
|
||||
puzzleService.submitAttempt(request.getAttempts(), request.getSolved());
|
||||
String message = request.getSolved() ?
|
||||
"Congratulations! You solved today's puzzle!" :
|
||||
"Better luck next time!";
|
||||
return ResponseEntity.ok(ApiResponse.success(message));
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.ok(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/leaderboard")
|
||||
@Operation(summary = "Get daily leaderboard", description = "Returns leaderboard for today's puzzle ordered by submission time")
|
||||
public ResponseEntity<ApiResponse<List<LeaderboardEntry>>> getLeaderboard() {
|
||||
try {
|
||||
List<PuzzleAttempt> attempts = puzzleService.getTodaysLeaderboard();
|
||||
|
||||
List<LeaderboardEntry> leaderboard = attempts.stream()
|
||||
.map(attempt -> new LeaderboardEntry(
|
||||
attempt.getUser().getDisplayName(),
|
||||
attempt.getUser().getUsername(),
|
||||
attempt.getAttempts(),
|
||||
attempt.isSolved(),
|
||||
attempt.getSubmittedAt()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(leaderboard));
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.ok(ApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package online.wesal.wesal.dto;
|
||||
|
||||
public class DailyChallengeResponse {
|
||||
|
||||
private String wordle;
|
||||
private boolean solved;
|
||||
|
||||
public DailyChallengeResponse() {}
|
||||
|
||||
public DailyChallengeResponse(String wordle, boolean solved) {
|
||||
this.wordle = wordle;
|
||||
this.solved = solved;
|
||||
}
|
||||
|
||||
public String getWordle() {
|
||||
return wordle;
|
||||
}
|
||||
|
||||
public void setWordle(String wordle) {
|
||||
this.wordle = wordle;
|
||||
}
|
||||
|
||||
public boolean isSolved() {
|
||||
return solved;
|
||||
}
|
||||
|
||||
public void setSolved(boolean solved) {
|
||||
this.solved = solved;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package online.wesal.wesal.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class LeaderboardEntry {
|
||||
|
||||
private String displayName;
|
||||
private String username;
|
||||
private int attempts;
|
||||
private boolean solved;
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
public LeaderboardEntry() {}
|
||||
|
||||
public LeaderboardEntry(String displayName, String username, int attempts, boolean solved, LocalDateTime submittedAt) {
|
||||
this.displayName = displayName;
|
||||
this.username = username;
|
||||
this.attempts = attempts;
|
||||
this.solved = solved;
|
||||
this.submittedAt = submittedAt;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public int getAttempts() {
|
||||
return attempts;
|
||||
}
|
||||
|
||||
public void setAttempts(int attempts) {
|
||||
this.attempts = attempts;
|
||||
}
|
||||
|
||||
public boolean isSolved() {
|
||||
return solved;
|
||||
}
|
||||
|
||||
public void setSolved(boolean solved) {
|
||||
this.solved = solved;
|
||||
}
|
||||
|
||||
public LocalDateTime getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
|
||||
public void setSubmittedAt(LocalDateTime submittedAt) {
|
||||
this.submittedAt = submittedAt;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package online.wesal.wesal.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public class PuzzleAttemptRequest {
|
||||
|
||||
@NotNull
|
||||
@Min(1)
|
||||
@Max(6)
|
||||
private Integer attempts;
|
||||
|
||||
@NotNull
|
||||
private Boolean solved;
|
||||
|
||||
public PuzzleAttemptRequest() {}
|
||||
|
||||
public PuzzleAttemptRequest(Integer attempts, Boolean solved) {
|
||||
this.attempts = attempts;
|
||||
this.solved = solved;
|
||||
}
|
||||
|
||||
public Integer getAttempts() {
|
||||
return attempts;
|
||||
}
|
||||
|
||||
public void setAttempts(Integer attempts) {
|
||||
this.attempts = attempts;
|
||||
}
|
||||
|
||||
public Boolean getSolved() {
|
||||
return solved;
|
||||
}
|
||||
|
||||
public void setSolved(Boolean solved) {
|
||||
this.solved = solved;
|
||||
}
|
||||
}
|
||||
50
backend/src/main/java/online/wesal/wesal/entity/Puzzle.java
Normal file
50
backend/src/main/java/online/wesal/wesal/entity/Puzzle.java
Normal file
@ -0,0 +1,50 @@
|
||||
package online.wesal.wesal.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "puzzles")
|
||||
public class Puzzle {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private LocalDate date;
|
||||
|
||||
@Column(nullable = false, length = 5)
|
||||
private String word;
|
||||
|
||||
public Puzzle() {}
|
||||
|
||||
public Puzzle(LocalDate date, String word) {
|
||||
this.date = date;
|
||||
this.word = word;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getWord() {
|
||||
return word;
|
||||
}
|
||||
|
||||
public void setWord(String word) {
|
||||
this.word = word;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
package online.wesal.wesal.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "puzzle_attempts")
|
||||
public class PuzzleAttempt {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "puzzle_id", nullable = false)
|
||||
private Puzzle puzzle;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int attempts;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean solved;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
public PuzzleAttempt() {}
|
||||
|
||||
public PuzzleAttempt(User user, Puzzle puzzle, int attempts, boolean solved) {
|
||||
this.user = user;
|
||||
this.puzzle = puzzle;
|
||||
this.attempts = attempts;
|
||||
this.solved = solved;
|
||||
this.submittedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public Puzzle getPuzzle() {
|
||||
return puzzle;
|
||||
}
|
||||
|
||||
public void setPuzzle(Puzzle puzzle) {
|
||||
this.puzzle = puzzle;
|
||||
}
|
||||
|
||||
public int getAttempts() {
|
||||
return attempts;
|
||||
}
|
||||
|
||||
public void setAttempts(int attempts) {
|
||||
this.attempts = attempts;
|
||||
}
|
||||
|
||||
public boolean isSolved() {
|
||||
return solved;
|
||||
}
|
||||
|
||||
public void setSolved(boolean solved) {
|
||||
this.solved = solved;
|
||||
}
|
||||
|
||||
public LocalDateTime getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
|
||||
public void setSubmittedAt(LocalDateTime submittedAt) {
|
||||
this.submittedAt = submittedAt;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package online.wesal.wesal.repository;
|
||||
|
||||
import online.wesal.wesal.entity.Puzzle;
|
||||
import online.wesal.wesal.entity.PuzzleAttempt;
|
||||
import online.wesal.wesal.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface PuzzleAttemptRepository extends JpaRepository<PuzzleAttempt, Long> {
|
||||
Optional<PuzzleAttempt> findByUserAndPuzzle(User user, Puzzle puzzle);
|
||||
|
||||
@Query("SELECT pa FROM PuzzleAttempt pa WHERE pa.puzzle = :puzzle ORDER BY pa.submittedAt ASC")
|
||||
List<PuzzleAttempt> findByPuzzleOrderBySubmittedAtAsc(@Param("puzzle") Puzzle puzzle);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package online.wesal.wesal.repository;
|
||||
|
||||
import online.wesal.wesal.entity.Puzzle;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface PuzzleRepository extends JpaRepository<Puzzle, Long> {
|
||||
Optional<Puzzle> findByDate(LocalDate date);
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
package online.wesal.wesal.service;
|
||||
|
||||
import online.wesal.wesal.entity.Puzzle;
|
||||
import online.wesal.wesal.entity.PuzzleAttempt;
|
||||
import online.wesal.wesal.entity.User;
|
||||
import online.wesal.wesal.repository.PuzzleAttemptRepository;
|
||||
import online.wesal.wesal.repository.PuzzleRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class PuzzleService {
|
||||
|
||||
@Autowired
|
||||
private PuzzleRepository puzzleRepository;
|
||||
|
||||
@Autowired
|
||||
private PuzzleAttemptRepository puzzleAttemptRepository;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
private static final List<String> WORD_LIST = Arrays.asList(
|
||||
"APPLE", "BEACH", "CHAIN", "DANCE", "EAGLE", "FLAME", "GRAPE", "HOUSE", "IMAGE", "JUDGE",
|
||||
"KNIFE", "LIGHT", "MUSIC", "NIGHT", "OCEAN", "PEACE", "QUEEN", "RIVER", "STAGE", "TIGER",
|
||||
"UNITY", "VOICE", "WATER", "YOUTH", "ZEBRA", "ANGEL", "BREAD", "CHAIR", "DREAM", "EARTH",
|
||||
"FIELD", "GLASS", "HEART", "INDEX", "JUICE", "KITE", "LEMON", "MAGIC", "NOVEL", "OPERA",
|
||||
"PLANT", "QUEST", "RADIO", "SMILE", "TOWER", "ULTRA", "VILLA", "WORLD", "YOUTH", "ZEBRA",
|
||||
"ABOUT", "ABOVE", "ABUSE", "ACTOR", "ACUTE", "ADMIT", "ADOPT", "ADULT", "AFTER", "AGAIN",
|
||||
"AGENT", "AGREE", "AHEAD", "ALARM", "ALBUM", "ALERT", "ALIEN", "ALIGN", "ALIKE", "ALIVE",
|
||||
"ALLOW", "ALONE", "ALONG", "ALTER", "AMONG", "ANGER", "ANGLE", "ANGRY", "APART", "APPLE",
|
||||
"APPLY", "ARENA", "ARGUE", "ARISE", "ARRAY", "ASIDE", "ASSET", "AVOID", "AWAKE", "AWARD",
|
||||
"AWARE", "BADLY", "BAKER", "BASES", "BASIC", "BEACH", "BEGAN", "BEGIN", "BEING", "BELOW",
|
||||
"BENCH", "BILLY", "BIRTH", "BLACK", "BLAME", "BLANK", "BLIND", "BLOCK", "BLOOD", "BOARD",
|
||||
"BOOST", "BOOTH", "BOUND", "BRAIN", "BRAND", "BRASS", "BRAVE", "BREAD", "BREAK", "BREED",
|
||||
"BRIEF", "BRING", "BROAD", "BROKE", "BROWN", "BUILD", "BUILT", "BUYER", "CABLE", "CALIF",
|
||||
"CARRY", "CATCH", "CAUSE", "CHAIN", "CHAIR", "CHAOS", "CHARM", "CHART", "CHASE", "CHEAP",
|
||||
"CHECK", "CHEST", "CHIEF", "CHILD", "CHINA", "CHOSE", "CIVIL", "CLAIM", "CLASS", "CLEAN",
|
||||
"CLEAR", "CLICK", "CLIMB", "CLOCK", "CLOSE", "CLOUD", "COACH", "COAST", "COULD", "COUNT",
|
||||
"COURT", "COVER", "CRAFT", "CRASH", "CRAZY", "CREAM", "CRIME", "CROSS", "CROWD", "CROWN",
|
||||
"CRUDE", "CURVE", "CYCLE", "DAILY", "DAMAGE", "DANCE", "DATED", "DEALT", "DEATH", "DEBUG",
|
||||
"DELAY", "DEPTH", "DOING", "DOUBT", "DOZEN", "DRAFT", "DRAMA", "DRANK", "DRAWN", "DREAM",
|
||||
"DRESS", "DRILL", "DRINK", "DRIVE", "DROVE", "DYING", "EAGER", "EARLY", "EARTH", "EIGHT",
|
||||
"ELITE", "EMPTY", "ENEMY", "ENJOY", "ENTER", "ENTRY", "EQUAL", "ERROR", "EVENT", "EVERY",
|
||||
"EXACT", "EXIST", "EXTRA", "FAITH", "FALSE", "FAULT", "FIBER", "FIELD", "FIFTH", "FIFTY",
|
||||
"FIGHT", "FINAL", "FIRST", "FIXED", "FLASH", "FLEET", "FLOOR", "FLUID", "FOCUS", "FORCE",
|
||||
"FORTH", "FORTY", "FORUM", "FOUND", "FRAME", "FRANK", "FRAUD", "FRESH", "FRONT", "FRUIT",
|
||||
"FULLY", "FUNNY", "GIANT", "GIVEN", "GLASS", "GLOBE", "GOING", "GRACE", "GRADE", "GRAND",
|
||||
"GRANT", "GRASS", "GRAVE", "GREAT", "GREEN", "GROSS", "GROUP", "GROWN", "GUARD", "GUESS",
|
||||
"GUEST", "GUIDE", "HAPPY", "HARRY", "HEART", "HEAVY", "HENCE", "HENRY", "HORSE", "HOTEL",
|
||||
"HOUSE", "HUMAN", "IDEAL", "IMAGE", "INDEX", "INNER", "INPUT", "ISSUE", "JAPAN", "JIMMY",
|
||||
"JOINT", "JONES", "JUDGE", "KNOWN", "LABEL", "LARGE", "LASER", "LATER", "LAUGH", "LAYER",
|
||||
"LEARN", "LEASE", "LEAST", "LEAVE", "LEGAL", "LEVEL", "LEWIS", "LIGHT", "LIMIT", "LINKS",
|
||||
"LIVES", "LOCAL", "LOOSE", "LOWER", "LUCKY", "LUNCH", "LYING", "MAGIC", "MAJOR", "MAKER",
|
||||
"MARCH", "MARIA", "MATCH", "MAYBE", "MAYOR", "MEANT", "MEDIA", "METAL", "MIGHT", "MINOR",
|
||||
"MINUS", "MIXED", "MODEL", "MONEY", "MONTH", "MORAL", "MOTOR", "MOUNT", "MOUSE", "MOUTH",
|
||||
"MOVED", "MOVIE", "MUSIC", "NEEDS", "NEVER", "NEWLY", "NIGHT", "NOISE", "NORTH", "NOTED",
|
||||
"NOVEL", "NURSE", "OCCUR", "OCEAN", "OFFER", "OFTEN", "ORDER", "OTHER", "OUGHT", "PAINT",
|
||||
"PANEL", "PAPER", "PARTY", "PEACE", "PETER", "PHASE", "PHONE", "PHOTO", "PIANO", "PICKED",
|
||||
"PIECE", "PILOT", "PITCH", "PLACE", "PLAIN", "PLANE", "PLANT", "PLATE", "POINT", "POUND",
|
||||
"POWER", "PRESS", "PRICE", "PRIDE", "PRIME", "PRINT", "PRIOR", "PRIZE", "PROOF", "PROUD",
|
||||
"PROVE", "QUEEN", "QUICK", "QUIET", "QUITE", "RADIO", "RAISE", "RANGE", "RAPID", "RATIO",
|
||||
"REACH", "READY", "REALM", "REBEL", "REFER", "RELAX", "RELAY", "RIDER", "RIDGE", "RIGHT",
|
||||
"RIGID", "RIVAL", "RIVER", "ROBIN", "ROGER", "ROMAN", "ROUGH", "ROUND", "ROUTE", "ROYAL",
|
||||
"RURAL", "SCALE", "SCENE", "SCOPE", "SCORE", "SENSE", "SERVE", "SEVEN", "SHALL", "SHAPE",
|
||||
"SHARE", "SHARP", "SHEET", "SHELF", "SHELL", "SHIFT", "SHINE", "SHIRT", "SHOCK", "SHOOT",
|
||||
"SHORT", "SHOWN", "SIGHT", "SILLY", "SINCE", "SIXTH", "SIXTY", "SIZED", "SKILL", "SLEEP",
|
||||
"SLIDE", "SMALL", "SMART", "SMILE", "SMITH", "SMOKE", "SNAKE", "SNOW", "SOBER", "SORRY",
|
||||
"SOUND", "SOUTH", "SPACE", "SPARE", "SPEAK", "SPEED", "SPEND", "SPENT", "SPLIT", "SPOKE",
|
||||
"SPORT", "STAFF", "STAGE", "STAKE", "STAND", "START", "STATE", "STEAM", "STEEL", "STEEP",
|
||||
"STEER", "STERN", "STICK", "STILL", "STOCK", "STONE", "STOOD", "STORE", "STORM", "STORY",
|
||||
"STRIP", "STUCK", "STUDY", "STUFF", "STYLE", "SUGAR", "SUITE", "SUPER", "SWEET", "TABLE",
|
||||
"TAKEN", "TASTE", "TAXES", "TEACH", "TEAM", "TEETH", "TERRY", "THANK", "THEFT", "THEIR",
|
||||
"THEME", "THERE", "THESE", "THICK", "THING", "THINK", "THIRD", "THOSE", "THREE", "THREW",
|
||||
"THROW", "THUMB", "TIGER", "TIGHT", "TIMES", "TIRED", "TITLE", "TODAY", "TOPIC", "TOTAL",
|
||||
"TOUCH", "TOUGH", "TOWER", "TRACK", "TRADE", "TRAIN", "TREAT", "TREND", "TRIAL", "TRIBE",
|
||||
"TRICK", "TRIED", "TRIES", "TRIP", "TRUCK", "TRULY", "TRUNK", "TRUST", "TRUTH", "TWICE",
|
||||
"UNDER", "UNDUE", "UNION", "UNITY", "UNTIL", "UPPER", "UPSET", "URBAN", "USAGE", "USUAL",
|
||||
"VALUE", "VIDEO", "VIRUS", "VISIT", "VITAL", "VOCAL", "VOICE", "WASTE", "WATCH", "WATER",
|
||||
"WHEEL", "WHERE", "WHICH", "WHILE", "WHITE", "WHOLE", "WHOSE", "WOMAN", "WOMEN", "WORLD",
|
||||
"WORRY", "WORSE", "WORST", "WORTH", "WOULD", "WRITE", "WRONG", "WROTE", "YOUNG", "YOUTH"
|
||||
);
|
||||
|
||||
public Puzzle getTodaysPuzzle() {
|
||||
LocalDate today = LocalDate.now();
|
||||
Optional<Puzzle> existingPuzzle = puzzleRepository.findByDate(today);
|
||||
|
||||
if (existingPuzzle.isPresent()) {
|
||||
return existingPuzzle.get();
|
||||
}
|
||||
|
||||
String word = generateWordForDate(today);
|
||||
Puzzle puzzle = new Puzzle(today, word);
|
||||
return puzzleRepository.save(puzzle);
|
||||
}
|
||||
|
||||
private String generateWordForDate(LocalDate date) {
|
||||
Random random = new Random(date.toEpochDay());
|
||||
return WORD_LIST.get(random.nextInt(WORD_LIST.size()));
|
||||
}
|
||||
|
||||
public boolean hasUserSolvedToday() {
|
||||
User currentUser = userService.getCurrentUser();
|
||||
Puzzle todaysPuzzle = getTodaysPuzzle();
|
||||
|
||||
Optional<PuzzleAttempt> attempt = puzzleAttemptRepository.findByUserAndPuzzle(currentUser, todaysPuzzle);
|
||||
return attempt.isPresent();
|
||||
}
|
||||
|
||||
public PuzzleAttempt submitAttempt(int attempts, boolean solved) {
|
||||
User currentUser = userService.getCurrentUser();
|
||||
Puzzle todaysPuzzle = getTodaysPuzzle();
|
||||
|
||||
Optional<PuzzleAttempt> existingAttempt = puzzleAttemptRepository.findByUserAndPuzzle(currentUser, todaysPuzzle);
|
||||
if (existingAttempt.isPresent()) {
|
||||
throw new RuntimeException("You've already attempted the daily challenge");
|
||||
}
|
||||
|
||||
PuzzleAttempt attempt = new PuzzleAttempt(currentUser, todaysPuzzle, attempts, solved);
|
||||
return puzzleAttemptRepository.save(attempt);
|
||||
}
|
||||
|
||||
public List<PuzzleAttempt> getTodaysLeaderboard() {
|
||||
Puzzle todaysPuzzle = getTodaysPuzzle();
|
||||
return puzzleAttemptRepository.findByPuzzleOrderBySubmittedAtAsc(todaysPuzzle);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'pages/feed_page.dart';
|
||||
import 'pages/invitations_page.dart';
|
||||
import 'pages/profile_page.dart';
|
||||
import 'pages/wordle_page.dart';
|
||||
import 'pages/puzzles_page.dart';
|
||||
import '../services/invitations_service.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@ -17,7 +17,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
final List<Widget> _pages = [
|
||||
FeedPage(),
|
||||
InvitationsPage(),
|
||||
WordlePage(),
|
||||
PuzzlesPage(),
|
||||
ProfilePage(),
|
||||
];
|
||||
|
||||
|
||||
280
frontend/lib/screens/pages/leaderboard_page.dart
Normal file
280
frontend/lib/screens/pages/leaderboard_page.dart
Normal file
@ -0,0 +1,280 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LeaderboardPage extends StatelessWidget {
|
||||
LeaderboardPage({Key? key}) : super(key: key);
|
||||
|
||||
// Sample JSON data structure for leaderboard
|
||||
final List<Map<String, dynamic>> leaderboardData = [
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Ahmed Hassan",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 3,
|
||||
"time": "2025-01-15T08:45:23.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Sarah Abdullah",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 4,
|
||||
"time": "2025-01-15T09:12:45.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Omar Al-Rashid",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 2,
|
||||
"time": "2025-01-15T08:23:12.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Fatima Al-Zahra",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 5,
|
||||
"time": "2025-01-15T09:34:56.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Khalid Mohammed",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 3,
|
||||
"time": "2025-01-15T08:56:34.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Layla Ibrahim",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 4,
|
||||
"time": "2025-01-15T09:18:27.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Sort leaderboard by time (fastest first)
|
||||
final sortedData = List<Map<String, dynamic>>.from(leaderboardData);
|
||||
sortedData.sort((a, b) {
|
||||
final timeA = DateTime.parse(a['time']);
|
||||
final timeB = DateTime.parse(b['time']);
|
||||
return timeA.compareTo(timeB);
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: Container(),
|
||||
centerTitle: true,
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'وصال',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w200,
|
||||
fontFamily: 'Blaka',
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Leaderboard',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Container(height: 1, color: Colors.grey[200]),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Leaderboard list
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.all(16),
|
||||
itemCount: sortedData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = sortedData[index];
|
||||
final user = entry['user'];
|
||||
final attempts = entry['attempts'];
|
||||
final timeString = entry['time'];
|
||||
final time = DateTime.parse(timeString);
|
||||
final rank = index + 1;
|
||||
|
||||
// Format time display
|
||||
final timeFormatted =
|
||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}';
|
||||
|
||||
// Determine rank styling
|
||||
Color rankColor = Colors.grey[600]!;
|
||||
IconData? rankIcon;
|
||||
if (rank == 1) {
|
||||
rankColor = Color(0xFFFFD700); // Gold
|
||||
rankIcon = Icons.emoji_events;
|
||||
} else if (rank == 2) {
|
||||
rankColor = Color(0xFFC0C0C0); // Silver
|
||||
rankIcon = Icons.emoji_events;
|
||||
} else if (rank == 3) {
|
||||
rankColor = Color(0xFFCD7F32); // Bronze
|
||||
rankIcon = Icons.emoji_events;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 12),
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border: rank <= 3
|
||||
? Border.all(
|
||||
color: rankColor.withOpacity(0.3),
|
||||
width: 2,
|
||||
)
|
||||
: Border.all(color: Colors.grey[200]!, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank indicator
|
||||
Container(
|
||||
width: 40,
|
||||
child: Column(
|
||||
children: [
|
||||
if (rankIcon != null)
|
||||
Icon(rankIcon, color: rankColor, size: 24)
|
||||
else
|
||||
Text(
|
||||
'#$rank',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: rankColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
|
||||
// User avatar
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: Color(0xFF6A4C93).withOpacity(0.1),
|
||||
child: Text(
|
||||
user['displayName'][0].toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
|
||||
// User details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
user['displayName'],
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
timeFormatted,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Icon(
|
||||
Icons.try_sms_star,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'$attempts attempts',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Return button
|
||||
Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Return',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
444
frontend/lib/screens/pages/puzzles_page.dart
Normal file
444
frontend/lib/screens/pages/puzzles_page.dart
Normal file
@ -0,0 +1,444 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'wordle_page.dart';
|
||||
|
||||
class PuzzlesPage extends StatefulWidget {
|
||||
const PuzzlesPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PuzzlesPageState createState() => _PuzzlesPageState();
|
||||
}
|
||||
|
||||
class _PuzzlesPageState extends State<PuzzlesPage> {
|
||||
String _currentView = 'main'; // main, leaderboard
|
||||
|
||||
void _showLeaderboardView() {
|
||||
setState(() {
|
||||
_currentView = 'leaderboard';
|
||||
});
|
||||
}
|
||||
|
||||
void _showMainView() {
|
||||
setState(() {
|
||||
_currentView = 'main';
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_currentView == 'leaderboard') {
|
||||
return _buildLeaderboardView();
|
||||
}
|
||||
return _buildMainView();
|
||||
}
|
||||
|
||||
Widget _buildMainView() {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// App Logo
|
||||
Text(
|
||||
'وصال',
|
||||
style: TextStyle(
|
||||
fontSize: 72,
|
||||
fontWeight: FontWeight.w200,
|
||||
fontFamily: 'Blaka',
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
|
||||
// Daily Challenge Title
|
||||
Text(
|
||||
'Daily Challenge',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Challenge timing info
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'Daily Wordle Challenge Open Until 11 AM',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 60),
|
||||
|
||||
// Big gamified PLAY button
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => WordlePage()),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.white, Colors.white.withOpacity(0.9)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: Offset(0, 8),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
blurRadius: 10,
|
||||
offset: Offset(-5, -5),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: Color(0xFF6A4C93).withOpacity(0.2),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.sports_esports,
|
||||
size: 32,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
'PLAY NOW',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6A4C93),
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 20,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40),
|
||||
|
||||
// Leaderboard button
|
||||
GestureDetector(
|
||||
onTap: _showLeaderboardView,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.leaderboard, color: Colors.white, size: 24),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Leaderboard',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLeaderboardView() {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Color(0xFF6A4C93)),
|
||||
onPressed: _showMainView,
|
||||
),
|
||||
centerTitle: true,
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'وصال',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w200,
|
||||
fontFamily: 'Blaka',
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Leaderboard',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Container(height: 1, color: Colors.grey[200]),
|
||||
),
|
||||
),
|
||||
body: _LeaderboardContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper class for Leaderboard content without the scaffold
|
||||
class _LeaderboardContent extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> leaderboardData = [
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Ahmed Hassan",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 3,
|
||||
"time": "2025-01-15T08:45:23.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Sarah Abdullah",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 4,
|
||||
"time": "2025-01-15T09:12:45.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Omar Al-Rashid",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 2,
|
||||
"time": "2025-01-15T08:23:12.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Fatima Al-Zahra",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 5,
|
||||
"time": "2025-01-15T09:34:56.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Khalid Mohammed",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 3,
|
||||
"time": "2025-01-15T08:56:34.000Z",
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"displayName": "Layla Ibrahim",
|
||||
"avatar": "https://via.placeholder.com/50",
|
||||
},
|
||||
"attempts": 4,
|
||||
"time": "2025-01-15T09:18:27.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Sort leaderboard by time (fastest first)
|
||||
final sortedData = List<Map<String, dynamic>>.from(leaderboardData);
|
||||
sortedData.sort((a, b) {
|
||||
final timeA = DateTime.parse(a['time']);
|
||||
final timeB = DateTime.parse(b['time']);
|
||||
return timeA.compareTo(timeB);
|
||||
});
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Leaderboard list
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.all(16),
|
||||
itemCount: sortedData.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = sortedData[index];
|
||||
final user = entry['user'];
|
||||
final attempts = entry['attempts'];
|
||||
final timeString = entry['time'];
|
||||
final time = DateTime.parse(timeString);
|
||||
final rank = index + 1;
|
||||
|
||||
// Format time display
|
||||
final timeFormatted =
|
||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}';
|
||||
|
||||
// Determine rank styling
|
||||
Color rankColor = Colors.grey[600]!;
|
||||
IconData? rankIcon;
|
||||
if (rank == 1) {
|
||||
rankColor = Color(0xFFFFD700); // Gold
|
||||
rankIcon = Icons.emoji_events;
|
||||
} else if (rank == 2) {
|
||||
rankColor = Color(0xFFC0C0C0); // Silver
|
||||
rankIcon = Icons.emoji_events;
|
||||
} else if (rank == 3) {
|
||||
rankColor = Color(0xFFCD7F32); // Bronze
|
||||
rankIcon = Icons.emoji_events;
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 12),
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border: rank <= 3
|
||||
? Border.all(color: rankColor.withOpacity(0.3), width: 2)
|
||||
: Border.all(color: Colors.grey[200]!, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank indicator
|
||||
Container(
|
||||
width: 40,
|
||||
child: Column(
|
||||
children: [
|
||||
if (rankIcon != null)
|
||||
Icon(rankIcon, color: rankColor, size: 24)
|
||||
else
|
||||
Text(
|
||||
'#$rank',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: rankColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
|
||||
// User avatar
|
||||
CircleAvatar(
|
||||
radius: 25,
|
||||
backgroundColor: Color(0xFF6A4C93).withOpacity(0.1),
|
||||
child: Text(
|
||||
user['displayName'][0].toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
|
||||
// User details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
user['displayName'],
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
timeFormatted,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Icon(
|
||||
Icons.try_sms_star,
|
||||
size: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'$attempts attempts',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum LetterState { empty, absent, present, correct }
|
||||
@ -255,7 +255,10 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: Color(0xFF6A4C93)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
centerTitle: true,
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user