Skip to content

Commit 844c875

Browse files
authored
Merge pull request #111 from RealTeamRocket/109-friendlist-page
109 friendlist page
2 parents 76d915e + f6de5f3 commit 844c875

9 files changed

Lines changed: 332 additions & 15 deletions

File tree

rocket-backend/internal/database/daily_steps_table.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111

1212
func (s *service) saveStepMilestoneActivities(userID uuid.UUID, oldSteps, newSteps int) {
1313
milestone := 2000
14-
for threshold := ((oldSteps/milestone)+1)*milestone; threshold <= newSteps; threshold += milestone {
14+
for threshold := ((oldSteps / milestone) + 1) * milestone; threshold <= newSteps; threshold += milestone {
1515
message := fmt.Sprintf("🎉 Has reached %d steps today! 🚀", threshold)
1616
_ = s.SaveActivity(userID, message)
1717
}
@@ -118,3 +118,20 @@ func (s *service) GetUserStatistics(userID uuid.UUID) ([]types.StepStatistic, er
118118

119119
return statistics, nil
120120
}
121+
122+
func (s *service) GetDailySteps(userID uuid.UUID) (int, error) {
123+
currentDate := time.Now().Format("2006-01-02")
124+
var steps int
125+
query := `SELECT steps_taken FROM daily_steps WHERE user_id = $1 AND date = $2`
126+
err := s.db.QueryRow(query, userID, currentDate).Scan(&steps)
127+
128+
if err != nil {
129+
if err.Error() == "sql: no rows in result set" {
130+
return 0, nil
131+
}
132+
logger.Error("Error retrieving daily steps: %v\n", err)
133+
return 0, fmt.Errorf("failed to retrieve daily steps: %w", err)
134+
}
135+
136+
return steps, nil
137+
}

rocket-backend/internal/database/database.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type Service interface {
4646
// daily_steps
4747
UpdateDailySteps(userID uuid.UUID, steps int) error
4848
GetUserStatistics(userID uuid.UUID) ([]types.StepStatistic, error)
49+
GetDailySteps(userID uuid.UUID) (int, error)
4950

5051
// settings
5152
GetSettingsByUserID(userID uuid.UUID) (*types.Settings, error)

rocket-backend/internal/server/friends_handlers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,12 @@ func (s *Server) GetAllFriendsHandler(c *gin.Context) {
122122
var friendsWithImages []types.UserWithImageDTO
123123
for _, fr := range friends {
124124
var f types.UserWithImageDTO
125+
var steps, _ = s.db.GetDailySteps(fr.ID)
125126
f.ID = fr.ID
126127
f.Username = fr.Username
127128
f.Email = fr.Email
128129
f.RocketPoints = fr.RocketPoints
130+
f.Steps = steps
129131

130132
userImage, imgErr := s.db.GetUserImage(fr.ID)
131133
if imgErr != nil {

rocket-backend/internal/server/user_handlers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,13 @@ func (s *Server) GetAllUsersHandler(c *gin.Context) {
166166
var usersWithImages []types.UserWithImageDTO
167167

168168
for _, user := range users {
169+
var steps, err = s.db.GetDailySteps(user.ID)
169170
var userWithImage types.UserWithImageDTO
170171
userWithImage.ID = user.ID
171172
userWithImage.Username = user.Username
172173
userWithImage.Email = user.Email
173174
userWithImage.RocketPoints = user.RocketPoints
175+
userWithImage.Steps = steps
174176
userImage, err := s.db.GetUserImage(user.ID)
175177

176178
if err != nil {

rocket-backend/internal/types/dto.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type UserWithImageDTO struct {
4141
RocketPoints int `json:"rocket_points"`
4242
ImageName string `json:"image_name"`
4343
ImageData string `json:"image_data"`
44+
Steps int `json:"steps"`
4445
}
4546

4647
type RunDataDTO struct {
@@ -73,11 +74,11 @@ type ChatMessage struct {
7374
}
7475

7576
type PlannedRunDTO struct {
76-
ID string `json:"id"`
77-
Route string `json:"route"`
78-
Name string `json:"name"`
79-
CreatedAt string `json:"created_at"`
80-
Distance float64 `json:"distance"`
77+
ID string `json:"id"`
78+
Route string `json:"route"`
79+
Name string `json:"name"`
80+
CreatedAt string `json:"created_at"`
81+
Distance float64 `json:"distance"`
8182
}
8283

8384
type DailyChallengeProgress struct {

website/src/api/backend-api.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ export default {
5656
return protectedAxiosApi.delete(`/runs/${id}`, { withCredentials: true });
5757
},
5858
savePlannedRun(route: string, name: string, distance: number): Promise<AxiosResponse> {
59-
return protectedAxiosApi.post(
60-
'/runs/plan',
61-
{ route, name, distance },
62-
{ withCredentials: true }
63-
);
59+
return protectedAxiosApi.post('/runs/plan', { route, name, distance }, { withCredentials: true });
6460
},
6561
getPlannedRuns(): Promise<AxiosResponse> {
6662
return protectedAxiosApi.get('/runs/plan', { withCredentials: true });
@@ -89,4 +85,13 @@ export default {
8985
getFollowers(id: string): Promise<AxiosResponse> {
9086
return protectedAxiosApi.get(`/followers/${id}`, { withCredentials: true });
9187
},
88+
addFriend(friendName: string): Promise<AxiosResponse> {
89+
return protectedAxiosApi.post('/friends/add', { friend_name: friendName }, { withCredentials: true });
90+
},
91+
deleteFriend(friendName: string): Promise<AxiosResponse> {
92+
return protectedAxiosApi.delete(`/friends/${encodeURIComponent(friendName)}`, { withCredentials: true });
93+
},
94+
getAllUsers(): Promise<AxiosResponse> {
95+
return protectedAxiosApi.get('/users', { withCredentials: true });
96+
},
9297
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<template>
2+
<div class="friend-card">
3+
<img v-if="friend.image" :src="friend.image" class="friend-avatar" />
4+
<div v-else class="friend-avatar-placeholder">{{ initials }}</div>
5+
<div class="friend-info">
6+
<div class="friend-name">{{ friend.username }}</div>
7+
<div class="friend-email">{{ friend.email }}</div>
8+
<div v-if="isFriend">
9+
<div class="friend-points">🚀 {{ friend.rocketPoints ?? 0 }}</div>
10+
<div class="friend-steps">👣 {{ friend.steps ?? 0 }}</div>
11+
</div>
12+
</div>
13+
<button v-if="isFriend" class="unfollow-btn" @click="$emit('unfollow', friend.id)"> Unfollow </button>
14+
<button v-else class="follow-btn" @click="$emit('add-friend', friend)"> Follow </button>
15+
</div>
16+
</template>
17+
18+
<script setup lang="ts">
19+
const props = defineProps<{
20+
friend: { id: string, username: string, email: string, rocketPoints: number, image?: string, steps?: number },
21+
isFriend?: boolean
22+
}>();
23+
24+
const initials = props.friend.username
25+
? props.friend.username.split(' ').map(n => n[0]).join('').toUpperCase()
26+
: '';
27+
</script>
28+
29+
<style scoped>
30+
.friend-card {
31+
display: flex;
32+
align-items: center;
33+
background: linear-gradient(90deg, #e0e7ff 0%, #f3f8ff 100%);
34+
border-radius: 1rem;
35+
box-shadow: 0 2px 8px rgba(30,60,114,0.08);
36+
padding: 1rem 1.5rem;
37+
min-width: 600px;
38+
max-width: 700px;
39+
width: 100%;
40+
margin: 0.5rem;
41+
position: relative;
42+
}
43+
.friend-avatar, .friend-avatar-placeholder {
44+
width: 60px;
45+
height: 60px;
46+
border-radius: 50%;
47+
object-fit: cover;
48+
background: #c7d2fe;
49+
display: flex;
50+
align-items: center;
51+
justify-content: center;
52+
font-size: 2rem;
53+
color: #374151;
54+
margin-right: 1rem;
55+
}
56+
.friend-info {
57+
flex: 1;
58+
}
59+
.friend-name {
60+
font-size: 2.0rem;
61+
font-weight: 600;
62+
color: #2a5298;
63+
}
64+
.friend-email {
65+
font-size: 0.95rem;
66+
color: #64748b;
67+
}
68+
.friend-points {
69+
font-size: 0.95rem;
70+
color: #64748b;
71+
}
72+
.friend-steps{
73+
font-size: 0.95rem;
74+
color: #64748b;
75+
}
76+
.unfollow-btn {
77+
background: #fff;
78+
border: 1px solid #ef4444;
79+
color: #ef4444;
80+
border-radius: 0.5rem;
81+
padding: 0.4rem 1rem;
82+
font-weight: 500;
83+
cursor: pointer;
84+
transition: background 0.2s, color 0.2s;
85+
position: absolute;
86+
bottom: 1rem;
87+
right: 1rem;
88+
}
89+
.unfollow-btn:hover {
90+
background: #ef4444;
91+
color: #fff;
92+
}
93+
.follow-btn {
94+
background: #22c55e;
95+
border: 1px solid #22c55e;
96+
color: #fff;
97+
border-radius: 0.5rem;
98+
padding: 0.4rem 1rem;
99+
font-weight: 500;
100+
cursor: pointer;
101+
transition: background 0.2s, color 0.2s;
102+
position: absolute;
103+
bottom: 1rem;
104+
right: 1rem;
105+
}
106+
.follow-btn:hover {
107+
background: #16a34a;
108+
border-color: #16a34a;
109+
}
110+
</style>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<template>
2+
<div class="search-bar">
3+
<input
4+
type="text"
5+
v-model="search"
6+
placeholder="🔍 Search new friends..."
7+
@input="$emit('update:search', search)"
8+
/>
9+
</div>
10+
</template>
11+
12+
<script setup lang="ts">
13+
import { ref } from 'vue';
14+
const search = ref('');
15+
</script>
16+
17+
<style scoped>
18+
.search-bar {
19+
width: 100%;
20+
margin-bottom: 1.5rem;
21+
display: flex;
22+
justify-content: center;
23+
}
24+
input {
25+
width: 100%;
26+
max-width: 500px;
27+
padding: 0.7rem 1.2rem;
28+
border-radius: 1rem;
29+
border: 1px solid #c7d2fe;
30+
font-size: 1.1rem;
31+
background: #f3f8ff;
32+
outline: none;
33+
transition: border 0.2s;
34+
}
35+
input:focus {
36+
border: 1.5px solid #4f46e5;
37+
}
38+
</style>

0 commit comments

Comments
 (0)