useBestFriends();
This commit is contained in:
198
components/roblox/FriendCarousel.tsx
Normal file
198
components/roblox/FriendCarousel.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
||||
import { useFriendsHome } from "@/hooks/roblox/useFriends";
|
||||
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import LazyLoadedImage from "../util/LazyLoadedImage";
|
||||
import { StupidHoverThing } from "../util/MiscStuff";
|
||||
import { VerifiedIcon } from "./RobloxIcons";
|
||||
|
||||
export function FriendCarousel({
|
||||
friends: friendsUnsorted,
|
||||
title,
|
||||
dontSortByActivity,
|
||||
...props
|
||||
}: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
title: string;
|
||||
dontSortByActivity?: boolean;
|
||||
friends:
|
||||
| {
|
||||
hasVerifiedBadge: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
}[]
|
||||
| null
|
||||
| false;
|
||||
}) {
|
||||
const acct = useCurrentAccount();
|
||||
const presence = useFriendsPresence(
|
||||
(!!friendsUnsorted ? friendsUnsorted : []).map((f) => f.id)
|
||||
);
|
||||
|
||||
const [friendsLabel, setFriendsLabel] = useState<string>("");
|
||||
|
||||
const [friends, setFriends] = useState<
|
||||
{
|
||||
hasVerifiedBadge: boolean;
|
||||
id: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let numStudio = 0;
|
||||
let numGame = 0;
|
||||
let numOnline = 0;
|
||||
for (const friend of friendsUnsorted || []) {
|
||||
const st = presence.find((c) => c.userId === friend.id);
|
||||
switch (st?.userPresenceType || 0) {
|
||||
case 1:
|
||||
numOnline += 1;
|
||||
break;
|
||||
case 2:
|
||||
numGame += 1;
|
||||
break;
|
||||
case 3:
|
||||
numStudio += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setFriendsLabel(
|
||||
[
|
||||
`${friends.length}`,
|
||||
numGame === 0 ? null : `${numGame} in-game`,
|
||||
numStudio === 0 ? null : `${numStudio} studio`
|
||||
]
|
||||
.filter((a) => !!a)
|
||||
.join(" | ")
|
||||
);
|
||||
|
||||
if (!friendsUnsorted) {
|
||||
setFriends([]);
|
||||
return;
|
||||
}
|
||||
setFriends(
|
||||
friendsUnsorted.sort((a, b) => {
|
||||
if (!!dontSortByActivity) return -10;
|
||||
const userStatusA = presence.find((c) => c.userId === a.id);
|
||||
const userStatusB = presence.find((c) => c.userId === b.id);
|
||||
|
||||
return (
|
||||
(userStatusB?.userPresenceType || 0) -
|
||||
(userStatusA?.userPresenceType || 0)
|
||||
);
|
||||
})
|
||||
);
|
||||
}, [friendsUnsorted, presence, dontSortByActivity]);
|
||||
|
||||
if (!friends || friends.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
{/* <button onClick={()=>console.log(acct,presence,friends)}>debug</button> */}
|
||||
<h1 className="text-2xl pt-4 pl-4 -mb-4">
|
||||
{title}{" "}
|
||||
<span className="text-overlay1 text-sm pl-2">{friendsLabel}</span>
|
||||
</h1>
|
||||
<div className="rounded-xl flex flex-col gap-2 px-4 no-scrollbar">
|
||||
<div
|
||||
className="flex p-8 items-center gap-4 overflow-x-auto overflow-y-visible no-scrollbar pb-2 -mx-4 w-screen scrollbar-thin scrollbar-thumb-surface2 scrollbar-track-surface0"
|
||||
style={{
|
||||
scrollSnapType: "x mandatory",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
scrollbarWidth: "none"
|
||||
}}
|
||||
>
|
||||
{/* <div className="w-8" /> */}
|
||||
{friends.map((a) => {
|
||||
const userStatus = presence.find(
|
||||
(b) => b.userId === a.id
|
||||
);
|
||||
const userPresence = userStatus?.userPresenceType || 0;
|
||||
const borderColor =
|
||||
userPresence === 1
|
||||
? "border-blue/25 bg-blue/25"
|
||||
: userPresence === 2
|
||||
? "border-green/25 bg-green/25"
|
||||
: userPresence === 3
|
||||
? "border-yellow/25 bg-yellow/25"
|
||||
: userPresence === 0
|
||||
? "border-surface2/25 bg-surface2/25"
|
||||
: "border-red/25 bg-red/25";
|
||||
const textColor =
|
||||
userPresence === 1
|
||||
? "text-blue"
|
||||
: userPresence === 2
|
||||
? "text-green"
|
||||
: userPresence === 3
|
||||
? "text-yellow"
|
||||
: userPresence === 0
|
||||
? "text-surface2"
|
||||
: "text-red";
|
||||
const fillColor =
|
||||
userPresence === 1
|
||||
? "fill-blue"
|
||||
: userPresence === 2
|
||||
? "fill-green"
|
||||
: userPresence === 3
|
||||
? "fill-yellow"
|
||||
: userPresence === 0
|
||||
? "fill-surface2"
|
||||
: "fill-red";
|
||||
|
||||
return (
|
||||
<StupidHoverThing
|
||||
key={a.id}
|
||||
delayDuration={0}
|
||||
text={
|
||||
<div className="text-center items-center justify-center content-center">
|
||||
<span className="space-x-1 flex items-center">
|
||||
<p>{a.displayName || a.name}</p>
|
||||
{!a.hasVerifiedBadge ? (
|
||||
<VerifiedIcon
|
||||
useDefault
|
||||
className={`w-4 h-4 shrink-0`}
|
||||
/>
|
||||
) : null}
|
||||
{ userPresence >= 2 ? <p>{userStatus?.lastLocation}</p> : <></>}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
key={a.id}
|
||||
className="flex flex-col min-w-[6.5rem]"
|
||||
>
|
||||
<LazyLoadedImage
|
||||
imgId={`AvatarHeadShot_${a.id}`}
|
||||
alt={a.name}
|
||||
className={`w-24 h-24 rounded-full border-2 ${borderColor} object-cover shadow-xl`}
|
||||
/>
|
||||
<span
|
||||
className={`text-xs ${textColor} mt-1 text-center flex items-center justify-center gap-1 max-w-[6.5rem] overflow-hidden line-clamp-2`}
|
||||
>
|
||||
<span className="line-clamp-1 overflow-hidden text-ellipsis">
|
||||
{a.displayName || a.name}
|
||||
</span>
|
||||
{!a.hasVerifiedBadge ? (
|
||||
<VerifiedIcon
|
||||
className={`text-base ${fillColor} w-3 h-3 shrink-0`}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</StupidHoverThing>
|
||||
);
|
||||
})}
|
||||
{/* <div className="w-8" /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
components/roblox/FriendsOnline.tsx
Normal file
26
components/roblox/FriendsOnline.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useFriendsHome } from "@/hooks/roblox/useFriends";
|
||||
import React from "react";
|
||||
import { FriendCarousel } from "./FriendCarousel";
|
||||
import { useBestFriends } from "@/hooks/roblox/useBestFriends";
|
||||
|
||||
export function FriendsHomeSect(
|
||||
props: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>
|
||||
) {
|
||||
const friends = useFriendsHome();
|
||||
|
||||
return <FriendCarousel {...props} title="Friends" friends={friends} />;
|
||||
}
|
||||
|
||||
export function BestFriendsHomeSect(
|
||||
props: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>
|
||||
) {
|
||||
const friends = useBestFriends();
|
||||
|
||||
return <FriendCarousel {...props} title="Best Friends" dontSortByActivity friends={friends} />;
|
||||
}
|
||||
101
components/roblox/GameCard.tsx
Normal file
101
components/roblox/GameCard.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { ContentMetadata } from "@/lib/omniRecommendation";
|
||||
import LazyLoadedImage from "../util/LazyLoadedImage";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger
|
||||
} from "../ui/context-menu";
|
||||
import { ContextMenuItem } from "@radix-ui/react-context-menu";
|
||||
import React from "react";
|
||||
|
||||
interface GameCardProps {
|
||||
game: ContentMetadata;
|
||||
}
|
||||
|
||||
export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<div className="overflow-hidden aspect-video relative bg-muted rounded-2xl">
|
||||
<div className="overflow-hidden">
|
||||
{game.primaryMediaAsset ? (
|
||||
<LazyLoadedImage
|
||||
imgId={
|
||||
"GameThumbnail_" +
|
||||
game.rootPlaceId.toString()
|
||||
}
|
||||
alt={game.name}
|
||||
className="object-fill w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-muted">
|
||||
<span className="text-muted-foreground">
|
||||
{":("}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-blue bg-base font-mono flex right-2 bottom-2 absolute rounded-lg px-2 py-1">
|
||||
{game.playerCount.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="max-w-[512px] p-2 space-y-1">
|
||||
<ContextMenuItem
|
||||
disabled
|
||||
className="text-s font-bold text-muted-foreground"
|
||||
>
|
||||
{game.name}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled
|
||||
className="text-xs text-subtext0 text-muted-foreground"
|
||||
>
|
||||
{Math.round(
|
||||
(game.totalUpVotes /
|
||||
(game.totalUpVotes + game.totalDownVotes)) *
|
||||
100
|
||||
)}
|
||||
% rating - {game.playerCount.toLocaleString()} playing
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
disabled
|
||||
className="pb-1 text-xs text-subtext0 text-muted-foreground"
|
||||
>
|
||||
{game.ageRecommendationDisplayName || ""}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem>
|
||||
<a href={`https://roblox.com/games/${game.rootPlaceId}`}>
|
||||
Open URL
|
||||
</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
window.location.href = `roblox://placeId=${game.rootPlaceId}`;
|
||||
}}
|
||||
>
|
||||
Play
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${game.rootPlaceId}`);
|
||||
}}
|
||||
>
|
||||
Copy rootPlaceId
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${game.universeId}`);
|
||||
}}
|
||||
>
|
||||
Copy universeId
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
});
|
||||
97
components/roblox/RobloxIcons.tsx
Normal file
97
components/roblox/RobloxIcons.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from "react";
|
||||
|
||||
export const PremiumIconSmall = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 16 16"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<path d="M14,14V2H2V16a2,2,0,0,1-2-2V2A2,2,0,0,1,2,0H14a2,2,0,0,1,2,2V14a2,2,0,0,1-2,2H8V14ZM12,6v6H8V10h2V6H6V16H4V4h8Z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="premium">
|
||||
<g clipPath="url(#clip-path)">
|
||||
<rect
|
||||
x="-5"
|
||||
y="-5"
|
||||
width="26"
|
||||
height="26"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const VerifiedIcon = ({
|
||||
useDefault,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & { useDefault?: boolean }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_8_46)">
|
||||
<rect
|
||||
x="5.88818"
|
||||
width="22.89"
|
||||
height="22.89"
|
||||
transform="rotate(15 5.88818 0)"
|
||||
fill={useDefault ? "#0066FF" : "currentFill"} /* #0066FF */
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M20.543 8.7508L20.549 8.7568C21.15 9.3578 21.15 10.3318 20.549 10.9328L11.817 19.6648L7.45 15.2968C6.85 14.6958 6.85 13.7218 7.45 13.1218L7.457 13.1148C8.058 12.5138 9.031 12.5138 9.633 13.1148L11.817 15.2998L18.367 8.7508C18.968 8.1498 19.942 8.1498 20.543 8.7508Z"
|
||||
fill={useDefault ? "white" : "currentColor"} /* white */
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8_46">
|
||||
<rect width="28" height="28" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RobuxIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g id="robux_28_dark">
|
||||
<path
|
||||
d="M23.402,5.571 C25.009,6.499 26,8.215 26,10.071 L26,17.927 C26,19.784 25.009,21.499 23.402,22.427 L16.597,26.356 C14.99,27.284 13.009,27.284 11.402,26.356 L4.597,22.427 C2.99,21.499 2,19.784 2,17.927 L2,10.071 C2,8.215 2.99,6.499 4.597,5.571 L11.402,1.643 C13.009,0.715 14.99,0.715 16.597,1.643 L23.402,5.571 Z M12.313,3.426 L5.686,7.252 C4.642,7.855 4,8.968 4,10.174 L4,17.825 C4,19.03 4.642,20.144 5.686,20.747 L12.313,24.572 C13.357,25.175 14.642,25.175 15.686,24.572 L22.313,20.747 C23.357,20.144 24,19.03 24,17.825 L24,10.174 C24,8.968 23.357,7.855 22.313,7.252 L15.686,3.426 C14.642,2.823 13.357,2.823 12.313,3.426 L12.313,3.426 Z M15.385,5.564 L20.614,8.582 C21.471,9.077 22,9.992 22,10.983 L22,17.02 C22,18.01 21.471,18.925 20.614,19.42 L15.385,22.439 C14.528,22.934 13.471,22.934 12.614,22.439 L7.385,19.42 C6.528,18.925 6,18.01 6,17.02 L6,10.983 C6,9.992 6.528,9.077 7.385,8.582 L12.614,5.564 C13.471,5.069 14.528,5.069 15.385,5.564 L15.385,5.564 Z M11,17.001 L17,17.001 L17,11.001 L11,11.001 L11,17.001 Z"
|
||||
id="Fill-1"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M51.402,5.571 C53.009,6.499 54,8.215 54,10.071 L54,17.927 C54,19.784 53.009,21.499 51.402,22.427 L44.597,26.356 C42.99,27.284 41.009,27.284 39.402,26.356 L32.597,22.427 C30.99,21.499 30,19.784 30,17.927 L30,10.071 C30,8.215 30.99,6.499 32.597,5.571 L39.402,1.643 C41.009,0.715 42.99,0.715 44.597,1.643 L51.402,5.571 Z M40.313,3.426 L33.686,7.252 C32.642,7.855 32,8.968 32,10.174 L32,17.825 C32,19.03 32.642,20.144 33.686,20.747 L40.313,24.572 C41.357,25.175 42.642,25.175 43.686,24.572 L50.313,20.747 C51.357,20.144 52,19.03 52,17.825 L52,10.174 C52,8.968 51.357,7.855 50.313,7.252 L43.686,3.426 C42.642,2.823 41.357,2.823 40.313,3.426 L40.313,3.426 Z M43.385,5.564 L48.614,8.582 C49.471,9.077 50,9.992 50,10.983 L50,17.02 C50,18.01 49.471,18.925 48.614,19.42 L43.385,22.439 C42.528,22.934 41.471,22.934 40.614,22.439 L35.385,19.42 C34.528,18.925 34,18.01 34,17.02 L34,10.983 C34,9.992 34.528,9.077 35.385,8.582 L40.614,5.564 C41.471,5.069 42.528,5.069 43.385,5.564 L43.385,5.564 Z M39,17.001 L45,17.001 L45,11.001 L39,11.001 L39,17.001 Z"
|
||||
id="Fill-1"
|
||||
fillOpacity="0.7"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<g id="Fill-1-Copy-2" transform="matrix(1, 0, 0, 1, 0, -0.5)">
|
||||
<use fill="black" fillOpacity="1" filter="url(#filter-3)"></use>
|
||||
<use fill="url(#linearGradient-1)" fillRule="evenodd"></use>
|
||||
<use fill="black" fillOpacity="1" filter="url(#filter-4)"></use>
|
||||
</g>
|
||||
<path
|
||||
d="M23.402,61.574 C25.009,62.502 26,64.218 26,66.074 L26,73.93 C26,75.787 25.009,77.502 23.402,78.43 L16.597,82.359 C14.99,83.287 13.009,83.287 11.402,82.359 L4.597,78.43 C2.99,77.502 2,75.787 2,73.93 L2,66.074 C2,64.218 2.99,62.502 4.597,61.574 L11.402,57.646 C13.009,56.718 14.99,56.718 16.597,57.646 L23.402,61.574 Z M12.313,59.429 L5.686,63.255 C4.642,63.858 4,64.971 4,66.177 L4,73.828 C4,75.033 4.642,76.147 5.686,76.75 L12.313,80.575 C13.357,81.178 14.642,81.178 15.686,80.575 L22.313,76.75 C23.357,76.147 24,75.033 24,73.828 L24,66.177 C24,64.971 23.357,63.858 22.313,63.255 L15.686,59.429 C14.642,58.826 13.357,58.826 12.313,59.429 L12.313,59.429 Z M15.385,61.567 L20.614,64.585 C21.471,65.08 22,65.995 22,66.986 L22,73.023 C22,74.013 21.471,74.928 20.614,75.423 L15.385,78.442 C14.528,78.937 13.471,78.937 12.614,78.442 L7.385,75.423 C6.528,74.928 6,74.013 6,73.023 L6,66.986 C6,65.995 6.528,65.08 7.385,64.585 L12.614,61.567 C13.471,61.072 14.528,61.072 15.385,61.567 L15.385,61.567 Z M11,73.004 L17,73.004 L17,67.004 L11,67.004 L11,73.004 Z"
|
||||
id="Fill-1-Copy-2"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
34
components/roblox/RobloxTooltips.tsx
Normal file
34
components/roblox/RobloxTooltips.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PremiumIconSmall, VerifiedIcon } from "./RobloxIcons";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger
|
||||
} from "../ui/tooltip";
|
||||
|
||||
export function RobloxPremiumSmall(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PremiumIconSmall {...props} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-surface0 text-text m-2">
|
||||
<p className="text-sm">Roblox Premium</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function RobloxVerifiedSmall(
|
||||
props: React.SVGProps<SVGSVGElement> & { useDefault?: boolean }
|
||||
) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<VerifiedIcon {...props} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-surface0 text-text m-2">
|
||||
<p className="text-sm">Verified</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user