Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function EmulateStageButton({ stageData }: EmulateStageButtonProps) {
isTime: isTime,
},
username: "Emited username",
stage: [stageData, new Date().getTime()],
stage: { data: stageData, timestamp: new Date().getTime() },
});
return (
<>
Expand Down
244 changes: 189 additions & 55 deletions frontend/src/components/overlay/achievements/Achievements.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { ObtainAchievementData, useSocketContext } from "@socket";
import { getDateFromSecondsToYMDHMS } from "@utils";
import {
ObtainAchievementDataWithCollectedAchievement,
ObtainAchievementDataWithProgressOnly,
useSocketContext,
} from "@socket";
import { getDateFromSecondsToYMDHMS, isObtainedAchievement } from "@utils";
import moment from "moment";
import { useEffect, useRef, useState } from "react";
import { viteBackendUrl } from "src/configs/envVariables";

type ObtainedAchievementStateType =
| ObtainAchievementDataWithCollectedAchievement
| ObtainAchievementDataWithProgressOnly;
export default function Achievements() {
const {
events: { obtainAchievement, obtainAchievementQueueInfo },
Expand All @@ -12,7 +18,7 @@ export default function Achievements() {
const wrapper = useRef<HTMLDivElement>(null);

const [obtainedAchievements, setObtainedAchievements] = useState<
ObtainAchievementData[]
ObtainedAchievementStateType[]
>([]);

const [showAchievementsQueue, setShowAchievementsQueue] = useState(false);
Expand All @@ -39,15 +45,26 @@ export default function Achievements() {
obtainAchievement.on((data) => {
setObtainedAchievements((prevState) => [data, ...prevState]);

const audioUrl = data.stage[0].sound;
if (audioUrl) {
audio.src = `${viteBackendUrl}/${audioUrl}`;
const options = {
audioUrl: "",
delay: 2500,
};
if (isObtainedAchievement(data)) {
options.audioUrl = data.stage.data.sound;
options.delay = data.stage.data.showTimeMs;
} else {
options.audioUrl = data.progressData.currentStage?.sound;
options.delay = data.progressData.currentStage?.showTimeMs;
}

if (options.audioUrl) {
audio.src = `${viteBackendUrl}/${options.audioUrl}`;
audio.play();
}

setTimeout(() => {
audio.pause();
}, data.stage[0].showTimeMs);
}, options.delay || 2500);
});

return () => {
Expand Down Expand Up @@ -93,55 +110,172 @@ export default function Achievements() {
{itemsQueLength}+
</div>

{obtainedAchievements.map(
({ stage, achievement: { name, isTime }, username, id }) => {
const [stageData, timestamp] = stage;

return (
<div
key={id}
className={`obtained-achievements-wrapper animated-achievement${
stageData.rarity ? `-${stageData.rarity}` : ""
}`}
>
<div className="achievements-overlay-background"></div>

<div className="obtained-achievements-content">
<div className="obtained-achievement-username">
{username}{" "}
<span>
obtained achievement <span>{name} </span>
</span>
</div>

<div className="obtained-achievement-details">
<div className="obtained-achievements-stage-name">
{stageData.name}
</div>

<div className="obtained-achievement-timestamp">
{moment(timestamp).format("HH:mm")}
</div>
<div className="obtained-achievements-goal">
Goal:
<span>
{isTime
? getDateFromSecondsToYMDHMS(stageData.goal)
: stageData.goal}
</span>
</div>
</div>
{obtainedAchievements.map((data) => {
return isObtainedAchievement(data) ? (
<AchievementDataBlock key={data.id} data={data} />
) : (
<AchievementProgressDataBlock key={data.id} data={data} />
);
})}
</div>
);
}

interface AchievementDataBlockProps {
data: ObtainAchievementDataWithCollectedAchievement;
}
function AchievementDataBlock({
data: {
stage: { data, timestamp },
id,
username,
achievement,
},
}: AchievementDataBlockProps) {
return (
<div
key={id}
className={`obtained-achievements-wrapper animated-achievement${
data.rarity ? `-${data.rarity}` : ""
}`}
>
<div className="achievements-overlay-background"></div>

<div className="obtained-achievements-content">
<div className="obtained-achievement-username">
{username}{" "}
<span>
obtained achievement <span>{achievement.name} </span>
</span>
</div>

<div className="obtained-achievement-details">
<div className="obtained-achievements-stage-name">{data.name}</div>

<div className="obtained-achievement-timestamp">
{moment(timestamp).format("HH:mm")}
</div>
<div className="obtained-achievements-goal">
Goal:
<span>
{achievement.isTime
? getDateFromSecondsToYMDHMS(data.goal)
: data.goal}
</span>
</div>
</div>
</div>
<div className="obtained-achievements-badge">
<img
src={`${viteBackendUrl}/${data.badge.imagesUrls.x64}`}
alt={data.name}
/>
</div>
</div>
);
}

interface AchievementProgressDataBlockProps {
data: ObtainAchievementDataWithProgressOnly;
}
function AchievementProgressDataBlock({
data: {
id,
progressData: { currentStage, nextStage, progress, timestamp },
username,
achievement,
},
}: AchievementProgressDataBlockProps) {
const renderProgressDivs = () => {
return (
<>
<div>
Progress:{" "}
<span>
{achievement.isTime
? getDateFromSecondsToYMDHMS(progress)
: progress}
</span>
</div>
<div>
{!nextStage ? null : (
<div>
Next:
<span>
{achievement.isTime
? getDateFromSecondsToYMDHMS(nextStage.goal)
: nextStage.goal}
</span>
</div>
)}
</div>
</>
);
};
return (
<div
key={id}
className={`obtained-achievements-wrapper achievement-progress-wrapper animated-achievement${
currentStage?.rarity ? `-${currentStage.rarity}` : ""
}`}
>
<div className="achievements-overlay-background"></div>

<div className="obtained-achievements-content">
<div className="obtained-achievement-username">
{username}
<span>
{" "}
made a progress in <span>{achievement.name} </span> achievement
</span>
</div>

<div className="obtained-achievement-details">
<div className="obtained-achievements-stage-name">
<div className="obtained-achievements-stage-name-header">STAGE</div>

<div>
{" "}
Current: <span>{currentStage?.name || "None"}</span>
</div>
{nextStage ? (
<div
className={`obtained-achievements-stage-name-next-stage ${
nextStage.rarity
? `animated-achievement-${nextStage.rarity}`
: ""
}`}
>
Next: <span> {nextStage.name}</span>
</div>
<div className="obtained-achievements-badge">
<img
src={`${viteBackendUrl}/${stageData.badge.imagesUrls.x64}`}
alt={stageData.name}
/>
) : (
<div className="obtained-achievements-stage-name-max-stage">
<span> Maxed out!</span>
</div>
</div>
);
}
)}
)}
</div>

<div className="obtained-achievement-timestamp">
{moment(timestamp).format("HH:mm")}
</div>
<div className="obtained-achievements-goal">
{renderProgressDivs()}
</div>
</div>
</div>
<div className="obtained-achievements-badge">
<img
className={`${
!currentStage ? "obtained-achievements-badge-not-achieved" : ""
}`}
src={`${viteBackendUrl}/${
currentStage
? currentStage?.badge.imagesUrls.x64
: nextStage?.badge.imagesUrls.x64
}`}
alt={currentStage?.name}
/>
</div>
</div>
);
}
25 changes: 25 additions & 0 deletions frontend/src/components/overlay/achievements/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@
@include animation-mixin(achievement-animation-4, $info-color);
@include animation-mixin(achievement-animation-5, orange);

.achievement-progress-wrapper {
.obtained-achievements-stage-name {
display: flex;
flex-direction: column;

.obtained-achievements-stage-name-next-stage {
filter: hue-rotate(90deg);
}

.obtained-achievements-stage-name-header {
letter-spacing: 0.2rem;
}
}
.obtained-achievements-stage-name-max-stage {
animation: wiggle 10s infinite;
span {
color: $danger-color !important;
}
}

.obtained-achievements-badge-not-achieved {
filter: grayscale(80%) !important;
}
}

.achievements-overlay-wrapper {
width: 100%;
max-height: 100%;
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/socket/emits.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Socket } from "socket.io-client";
import {
ClientToServerEvents,
ObtainAchievementData,
ObtainAchievementDataWithCollectedAchievement,
ServerToClientEvents,
SocketContexType,
} from "./types";
Expand Down Expand Up @@ -91,7 +91,9 @@ export const getSocketEmitsFunctions = (
getCustomRewards: () => {
socketConnection.emit("getCustomRewards");
},
emulateAchievement: (data: ObtainAchievementData) => {
emulateAchievement: (
data: ObtainAchievementDataWithCollectedAchievement
) => {
socketConnection.emit("emulateAchievement", data);
},
};
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/socket/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ export type {
EventAndUser,
CustomRewardData,
AudioStreamData,
ObtainAchievementData,
MessageServerData,
MessageServerDataBadgesPathsType,
ObtainAchievementDataWithCollectedAchievement,
ObtainAchievementDataWithProgressOnly,
} from "@socketTypes";

export type * from "./types";
19 changes: 18 additions & 1 deletion frontend/src/utils/socketData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { MessageServerData } from "@socketTypes";
import {
MessageServerData,
ObtainAchievementDataWithCollectedAchievement,
ObtainAchievementDataWithProgressOnly,
} from "@socketTypes";

type GetMessagesWithEmotesParams = Pick<
MessageServerData["messageData"],
Expand Down Expand Up @@ -80,3 +84,16 @@ export const getMessagesWithEmotes: GetMessagesWithEmotesFn = ({

return messagesAndEmotes;
};

//TODO: duplicate function is in backend socket.ts
export const isObtainedAchievement = (
data:
| ObtainAchievementDataWithCollectedAchievement
| ObtainAchievementDataWithProgressOnly
): data is ObtainAchievementDataWithCollectedAchievement => {
return (
(data as ObtainAchievementDataWithCollectedAchievement).stage !==
undefined &&
(data as ObtainAchievementDataWithCollectedAchievement).stage !== null
);
};
4 changes: 3 additions & 1 deletion server/src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export const getTwitchAuthUrl = () => {
"moderation:read",
"moderator:read:chatters",
"user:read:follows",
"moderator:read:followers"
"moderator:read:followers",
"channel:read:subscriptions",
"bits:read"
];
const response_type = "code";
const state = "c3ab8aa609ea11e793ae92361f002671";
Expand Down
Loading