mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-12 14:16:04 -05:00
Real friends data initial
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
---
|
||||
# sendou.ink-iulp
|
||||
title: FriendRepository implementation
|
||||
status: todo
|
||||
status: in-progress
|
||||
type: task
|
||||
priority: normal
|
||||
created_at: 2026-01-13T09:32:05Z
|
||||
updated_at: 2026-01-13T09:32:10Z
|
||||
updated_at: 2026-01-16T07:59:43Z
|
||||
parent: sendou.ink-255r
|
||||
---
|
||||
|
||||
@@ -37,8 +37,8 @@ Create repository for friend-related database operations.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Create FriendRepository.server.ts
|
||||
- [x] Create FriendRepository.server.ts
|
||||
- [x] Implement `findByUserIdWithActivity(userId)` for sidebar
|
||||
- [ ] Implement friendship CRUD
|
||||
- [ ] Implement friend request operations
|
||||
- [ ] Implement connections aggregation query
|
||||
- [ ] Unit tests for repository functions
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
# sendou.ink-ivaf
|
||||
title: Friends section in sidebar with quick invite
|
||||
status: todo
|
||||
type: feature
|
||||
created_at: 2026-01-16T10:17:54Z
|
||||
updated_at: 2026-01-16T10:17:54Z
|
||||
parent: sendou.ink-6eko
|
||||
---
|
||||
|
||||
Integrate friends into the sidebar, enabling quick invites and friend status viewing.
|
||||
|
||||
**Related:** sendou.ink-255r (Friends feature epic)
|
||||
|
||||
## Features
|
||||
|
||||
### Quick Invite
|
||||
- Users can send a "quick invite" to friends who have a partial SendouQ group that is currently looking for members
|
||||
- The invite should be fast/easy to send directly from the sidebar
|
||||
|
||||
### Expandable Friend Details
|
||||
When a user expands a friend entry in the sidebar, display:
|
||||
- User avatar
|
||||
- User name
|
||||
- Weapons (their current weapon pool or equipped weapons)
|
||||
- Current group info (if they're in a SendouQ group)
|
||||
- Any custom status text if available
|
||||
|
||||
For each of the friends groups' members
|
||||
|
||||
### Pending Requests Indicator
|
||||
- Show pending requests somewhere visible in the UI
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Design UI for friends list in sidebar
|
||||
- [ ] Add expand/collapse functionality for friend entries
|
||||
- [ ] Display friend details (avatar, name, weapons, status text)
|
||||
- [ ] Show current SendouQ group info for friends who are looking
|
||||
- [ ] Implement quick invite button for friends with partial groups
|
||||
- [ ] Add pending friend requests indicator
|
||||
- [ ] Handle loading and empty states
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
# sendou.ink-nucy
|
||||
title: Consistent avatar border radius in sidebar
|
||||
status: draft
|
||||
type: feature
|
||||
created_at: 2026-01-16T09:23:10Z
|
||||
updated_at: 2026-01-16T09:23:10Z
|
||||
---
|
||||
|
||||
Currently sidebar avatars have inconsistent border radius styles:
|
||||
- Some are fully rounded (circle)
|
||||
- Some are slightly rounded
|
||||
- Some have no background/different treatment
|
||||
|
||||
## Idea
|
||||
|
||||
Add a theme option for avatar border radius that applies globally to all avatars:
|
||||
- **round** - fully circular (border-radius: 50%)
|
||||
- **rounded** - slightly rounded corners
|
||||
- **square** - no border radius
|
||||
|
||||
This would provide visual consistency and give users control over their preferred avatar style.
|
||||
|
||||
## Alternative
|
||||
|
||||
Choose one and apply everywhere.
|
||||
|
||||
## Areas to check
|
||||
- Tournament/event avatars in sidebar
|
||||
- Friend list avatars
|
||||
- User avatars throughout the app
|
||||
- Team avatars
|
||||
|
||||
## Checklist
|
||||
- [ ] Audit current avatar border radius usage across codebase
|
||||
- [ ] Design CSS variable approach for avatar border radius
|
||||
- [ ] Add theme option to settings
|
||||
- [ ] Apply consistent border radius to all Avatar components
|
||||
- [ ] Test across different contexts (sidebar, profiles, etc.)
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
# sendou.ink-qu4s
|
||||
title: Show team and association members in friends section
|
||||
status: todo
|
||||
type: feature
|
||||
created_at: 2026-01-16T10:20:57Z
|
||||
updated_at: 2026-01-16T10:20:57Z
|
||||
parent: sendou.ink-255r
|
||||
---
|
||||
|
||||
Extend the friends section in the sidebar to also display:
|
||||
- Team members (from the user's team)
|
||||
- Association members (from the user's association)
|
||||
|
||||
## Sorting priority
|
||||
Friends should appear first in the list, followed by team members, then association members.
|
||||
|
||||
## Checklist
|
||||
- [ ] Query team members for the current user
|
||||
- [ ] Query association members for the current user
|
||||
- [ ] Combine friends, team members, and association members into a single list
|
||||
- [ ] Implement sorting logic: friends first, then team, then association
|
||||
- [ ] Handle duplicate entries (e.g., a friend who is also a team member should only appear once, as a friend)
|
||||
- [ ] Update sidebar UI to display the combined list
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
# sendou.ink-xsyy
|
||||
title: Sidebar friends section with real data
|
||||
status: todo
|
||||
status: in-progress
|
||||
type: task
|
||||
priority: normal
|
||||
created_at: 2026-01-13T09:32:29Z
|
||||
updated_at: 2026-01-13T09:32:29Z
|
||||
updated_at: 2026-01-16T08:04:13Z
|
||||
parent: sendou.ink-255r
|
||||
---
|
||||
|
||||
@@ -42,9 +43,9 @@ No grouping by relationship type in sidebar (mixed list with priority ordering).
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Update sidebar loader to fetch real connections
|
||||
- [ ] Implement priority sorting logic
|
||||
- [ ] Quota system for activity types
|
||||
- [ ] Update SideNavFriendItem component with real data
|
||||
- [x] Update sidebar loader to fetch real connections
|
||||
- [x] Implement basic priority sorting (SendouQ first, then tournament subs)
|
||||
- [x] Update SideNavFriendItem component with real data (URLs, avatars)
|
||||
- [ ] Full priority sorting logic with quota system
|
||||
- [ ] "See all" link to /friends page
|
||||
- [ ] Handle empty state (no friends yet)
|
||||
@@ -8,6 +8,7 @@ const dimensions = {
|
||||
xxxs: 16,
|
||||
xxxsm: 20,
|
||||
xxs: 24,
|
||||
xxsm: 32,
|
||||
xs: 36,
|
||||
sm: 44,
|
||||
xsm: 62,
|
||||
|
||||
@@ -357,8 +357,11 @@ function FriendsPanel({
|
||||
{friends.map((friend) => (
|
||||
<SideNavLink
|
||||
key={friend.id}
|
||||
to=""
|
||||
imageUrl={friend.avatarUrl}
|
||||
to={friend.url}
|
||||
user={{
|
||||
discordId: friend.discordId,
|
||||
discordAvatar: friend.discordAvatar,
|
||||
}}
|
||||
subtitle={friend.subtitle}
|
||||
badge={friend.badge}
|
||||
>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from "react-aria-components";
|
||||
import { Link } from "react-router";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { Image } from "./Image";
|
||||
import styles from "./SideNav.module.css";
|
||||
|
||||
@@ -77,6 +79,7 @@ export function SideNavLink({
|
||||
onClick,
|
||||
isActive,
|
||||
imageUrl,
|
||||
user,
|
||||
subtitle,
|
||||
badge,
|
||||
badgeVariant,
|
||||
@@ -86,6 +89,7 @@ export function SideNavLink({
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
isActive?: boolean;
|
||||
imageUrl?: string;
|
||||
user?: Pick<Tables["User"], "discordId" | "discordAvatar">;
|
||||
subtitle?: string;
|
||||
badge?: string;
|
||||
badgeVariant?: "default" | "warning";
|
||||
@@ -97,7 +101,9 @@ export function SideNavLink({
|
||||
onClick={onClick}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
>
|
||||
{imageUrl ? (
|
||||
{user ? (
|
||||
<Avatar user={user} size="xxsm" />
|
||||
) : imageUrl ? (
|
||||
<img src={imageUrl} alt="" className={styles.sideNavLinkImage} />
|
||||
) : null}
|
||||
<div className={styles.sideNavLinkContent}>
|
||||
|
||||
@@ -66,7 +66,6 @@ function useTimeFormat() {
|
||||
}
|
||||
|
||||
return date.toLocaleDateString(i18n.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
@@ -232,8 +231,11 @@ export function Layout({
|
||||
{friends.map((friend) => (
|
||||
<SideNavLink
|
||||
key={friend.id}
|
||||
to=""
|
||||
imageUrl={friend.avatarUrl}
|
||||
to={friend.url}
|
||||
user={{
|
||||
discordId: friend.discordId,
|
||||
discordAvatar: friend.discordAvatar,
|
||||
}}
|
||||
subtitle={friend.subtitle}
|
||||
badge={friend.badge}
|
||||
>
|
||||
|
||||
51
app/features/friends/FriendRepository.server.ts
Normal file
51
app/features/friends/FriendRepository.server.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { db } from "~/db/sql";
|
||||
|
||||
export async function findByUserIdWithActivity(userId: number) {
|
||||
const rows = await db
|
||||
.selectFrom("Friendship")
|
||||
.innerJoin("User", (join) =>
|
||||
join.on((eb) =>
|
||||
eb.or([
|
||||
eb.and([
|
||||
eb("Friendship.userOneId", "=", userId),
|
||||
eb("User.id", "=", eb.ref("Friendship.userTwoId")),
|
||||
]),
|
||||
eb.and([
|
||||
eb("Friendship.userTwoId", "=", userId),
|
||||
eb("User.id", "=", eb.ref("Friendship.userOneId")),
|
||||
]),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.leftJoin("TournamentSub", "TournamentSub.userId", "User.id")
|
||||
.leftJoin(
|
||||
"CalendarEvent",
|
||||
"CalendarEvent.tournamentId",
|
||||
"TournamentSub.tournamentId",
|
||||
)
|
||||
.leftJoin(
|
||||
"CalendarEventDate",
|
||||
"CalendarEventDate.eventId",
|
||||
"CalendarEvent.id",
|
||||
)
|
||||
.select([
|
||||
"User.id",
|
||||
"User.username",
|
||||
"User.discordId",
|
||||
"User.discordAvatar",
|
||||
"User.customUrl",
|
||||
"CalendarEvent.name as tournamentName",
|
||||
"TournamentSub.tournamentId",
|
||||
"CalendarEventDate.startTime as tournamentStartTime",
|
||||
])
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("Friendship.userOneId", "=", userId),
|
||||
eb("Friendship.userTwoId", "=", userId),
|
||||
]),
|
||||
)
|
||||
.orderBy("Friendship.createdAt", "desc")
|
||||
.execute();
|
||||
|
||||
return rows;
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { href, type LoaderFunctionArgs } from "react-router";
|
||||
import { getUser } from "~/features/auth/core/user.server";
|
||||
import * as FriendRepository from "~/features/friends/FriendRepository.server";
|
||||
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import * as ScrimPostRepository from "~/features/scrims/ScrimPostRepository.server";
|
||||
import { SendouQ } from "~/features/sendouq/core/SendouQ.server";
|
||||
import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants";
|
||||
import { RunningTournaments } from "~/features/tournament-bracket/core/RunningTournaments.server";
|
||||
import {
|
||||
navIconUrl,
|
||||
sendouQMatchPage,
|
||||
tournamentBracketsPage,
|
||||
tournamentMatchPage,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
|
||||
export type SidebarEvent = {
|
||||
@@ -36,15 +39,16 @@ export const loader = async (_args: LoaderFunctionArgs) => {
|
||||
roundName?: string;
|
||||
logoUrl: string | null;
|
||||
} | null,
|
||||
friends: getMockFriends(),
|
||||
friends: [] as ReturnType<typeof resolveFriends>,
|
||||
streams: getMockStreams(),
|
||||
};
|
||||
}
|
||||
|
||||
const [tournamentsData, scrimsData, ownGroup] = await Promise.all([
|
||||
const ownGroup = SendouQ.findOwnGroup(user.id);
|
||||
const [tournamentsData, scrimsData, friendsWithActivity] = await Promise.all([
|
||||
ShowcaseTournaments.frontPageTournamentsByUserId(user.id),
|
||||
ScrimPostRepository.findUserScrims(user.id),
|
||||
Promise.resolve(SendouQ.findOwnGroup(user.id)),
|
||||
FriendRepository.findByUserIdWithActivity(user.id),
|
||||
]);
|
||||
|
||||
const tournamentMatchStatus = resolveTournamentMatchStatus(user.id);
|
||||
@@ -85,13 +89,15 @@ export const loader = async (_args: LoaderFunctionArgs) => {
|
||||
.sort((a, b) => a.startTime - b.startTime)
|
||||
.slice(0, MAX_EVENTS_VISIBLE);
|
||||
|
||||
const friends = resolveFriends(friendsWithActivity);
|
||||
|
||||
return {
|
||||
events,
|
||||
matchStatus: ownGroup?.matchId
|
||||
? { matchId: ownGroup.matchId, url: sendouQMatchPage(ownGroup.matchId) }
|
||||
: null,
|
||||
tournamentMatchStatus,
|
||||
friends: getMockFriends(),
|
||||
friends,
|
||||
streams: getMockStreams(),
|
||||
};
|
||||
};
|
||||
@@ -144,44 +150,75 @@ function resolveTournamentMatchStatus(userId: number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMockFriends() {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: "Splat_Master",
|
||||
avatarUrl: "https://i.pravatar.cc/150?u=friend1",
|
||||
subtitle: "SendouQ",
|
||||
badge: "2/4",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "InklingPro",
|
||||
avatarUrl: "https://i.pravatar.cc/150?u=friend2",
|
||||
subtitle: "Lobby",
|
||||
badge: "2/8",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "OctoKing",
|
||||
avatarUrl: "https://i.pravatar.cc/150?u=friend3",
|
||||
subtitle: "In The Zone 22",
|
||||
badge: "3/4",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "TurfWarrior",
|
||||
avatarUrl: "https://i.pravatar.cc/150?u=friend4",
|
||||
subtitle: "SendouQ",
|
||||
badge: "1/4",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "RankedGrinder",
|
||||
avatarUrl: "https://i.pravatar.cc/150?u=friend5",
|
||||
subtitle: "Lobby",
|
||||
badge: "5/8",
|
||||
},
|
||||
];
|
||||
type FriendWithActivity = Awaited<
|
||||
ReturnType<typeof FriendRepository.findByUserIdWithActivity>
|
||||
>[number];
|
||||
|
||||
const MAX_FRIENDS_VISIBLE = 4;
|
||||
const SENDOUQ_QUOTA = 2;
|
||||
const TOURNAMENT_SUB_QUOTA = 2;
|
||||
|
||||
function resolveFriends(friendsWithActivity: FriendWithActivity[]) {
|
||||
type SidebarFriend = {
|
||||
id: number;
|
||||
name: string;
|
||||
discordId: string;
|
||||
discordAvatar: string | null;
|
||||
url: string;
|
||||
subtitle: string;
|
||||
badge: string;
|
||||
};
|
||||
|
||||
const sendouqFriends: SidebarFriend[] = [];
|
||||
const tournamentSubFriends: SidebarFriend[] = [];
|
||||
|
||||
for (const friend of friendsWithActivity) {
|
||||
const ownGroup = SendouQ.findOwnGroup(friend.id);
|
||||
|
||||
const url = userPage({
|
||||
discordId: friend.discordId,
|
||||
customUrl: friend.customUrl,
|
||||
});
|
||||
|
||||
if (ownGroup && ownGroup.members.length < FULL_GROUP_SIZE) {
|
||||
sendouqFriends.push({
|
||||
id: friend.id,
|
||||
name: friend.username,
|
||||
discordId: friend.discordId,
|
||||
discordAvatar: friend.discordAvatar,
|
||||
url,
|
||||
subtitle: "SendouQ",
|
||||
badge: `${ownGroup.members.length}/${FULL_GROUP_SIZE}`,
|
||||
});
|
||||
} else if (friend.tournamentName) {
|
||||
// this is temporary, will be replaced with "SQified tournament team creator"
|
||||
tournamentSubFriends.push({
|
||||
id: friend.id,
|
||||
name: friend.username,
|
||||
discordId: friend.discordId,
|
||||
discordAvatar: friend.discordAvatar,
|
||||
url,
|
||||
subtitle: friend.tournamentName,
|
||||
badge: `1/${FULL_GROUP_SIZE}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result: SidebarFriend[] = [];
|
||||
|
||||
const sendouqToShow = sendouqFriends.slice(0, SENDOUQ_QUOTA);
|
||||
const tournamentToShow = tournamentSubFriends.slice(0, TOURNAMENT_SUB_QUOTA);
|
||||
|
||||
result.push(...sendouqToShow, ...tournamentToShow);
|
||||
|
||||
const remaining = MAX_FRIENDS_VISIBLE - result.length;
|
||||
if (remaining > 0) {
|
||||
const extraSendouq = sendouqFriends.slice(SENDOUQ_QUOTA);
|
||||
const extraTournament = tournamentSubFriends.slice(TOURNAMENT_SUB_QUOTA);
|
||||
result.push(...[...extraSendouq, ...extraTournament].slice(0, remaining));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getMockStreams() {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user