Initial commit: Tapalka monorepo (bot, front, strapi)

This commit is contained in:
2026-05-21 11:54:44 +07:00
commit 9ec9485940
208 changed files with 58314 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
'use client';
import { ITask } from '@/api/tasks.api';
import { FC, useCallback } from 'react';
import styles from '@/components/tasks/Tasks.module.css';
import Image from 'next/image';
import { IFinishedTasks } from '@/interfaces/player.type';
import TaskContent from '@/components/tasks/TaskContent';
interface ITaskProps {
task: ITask;
finishedTask: IFinishedTasks | undefined;
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
handleOpenLink: (link: string, taskId: number) => Promise<void>;
}
const Task: FC<ITaskProps> = ({ task, finishedTask, handleClaimPrize, handleOpenLink }) => {
const handleTaskFunction = useCallback(() => {
if (!finishedTask) {
return handleOpenLink(task.attributes.link, task.id);
}
}, [handleOpenLink, finishedTask]);
return (
<div
className={
finishedTask && finishedTask.attributes.status === 'claimed'
? `${styles.taskClaimed} ${styles.task}`
: `${styles.task}`
}
onClick={handleTaskFunction}
>
{task?.attributes?.icon?.data && (
<div className={styles.taskIcon}>
<Image
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${task.attributes.icon.data.attributes.url}`}
alt={'task icon'}
width={32}
height={32}
/>
</div>
)}
<div className={styles.taskContent}>
<div
className={
finishedTask && finishedTask.attributes.status === 'claimed'
? `${styles.taskClaimedTitle} ${styles.taskTitle}`
: `${styles.taskTitle}`
}
>
<span>{task.attributes.title}</span>
</div>
<TaskContent
taskId={task.id}
link={task.attributes.link}
status={finishedTask?.attributes.status}
prize={task.attributes.prize}
handleClaimPrize={handleClaimPrize}
/>
</div>
</div>
);
};
export default Task;

View File

@@ -0,0 +1,61 @@
'use client';
import { FC, useState } from 'react';
import styles from '@/components/tasks/Tasks.module.css';
import Image from 'next/image';
interface ITaskContent {
taskId: number;
link: string;
status: string | undefined;
prize: number;
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
}
const TaskContent: FC<ITaskContent> = ({ taskId, status, prize, handleClaimPrize }) => {
const [isClaiming, setIsClaiming] = useState(false);
const onClaim = () => {
setIsClaiming(true);
handleClaimPrize(taskId, prize).catch(err => {
console.error(err);
setIsClaiming(false);
});
};
if (status) {
if (status === 'pending_prize') {
return (
<>
<button className={styles.claimPrize} onClick={onClaim} disabled={isClaiming}>
{`Получить +${prize}`}
<Image
src={'/icons/small-robucks-icon.svg'}
alt={'small coin icon'}
width={18}
height={18}
/>
</button>
</>
);
} else if (status === 'claimed') {
return (
<div className={styles.claimedPrize}>
<span>Выполнено</span>
</div>
);
}
} else {
return (
<div className={styles.taskPrize}>
<span>+ {prize}</span>
<Image
src={'/icons/small-robucks-icon.svg'}
alt={'small coin icon'}
width={18}
height={18}
/>
</div>
);
}
};
export default TaskContent;

View File

@@ -0,0 +1,109 @@
.tasks {
background: #101010;
padding-top: 45px;
font-family: var(--font-manrope);
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.tasksHeader {
padding-left: 1rem;
flex-shrink: 0;
}
.tasksHeader span {
font-weight: 600;
font-size: 19px;
color: white;
line-height: 21px;
}
.tasksBlock {
height: auto;
margin-top: 1rem;
padding: 30px 16px;
display: flex;
flex-direction: column;
gap: 8px;
background: #0E0E10;
border: 1px solid #18181A;
border-radius: 1.5rem;
overflow-y: auto;
}
.tasksBlock.overflow {
padding-bottom: 90px;
}
.task {
display: flex;
align-items: center;
background: #FFFFFF0D;
min-height: 75px;
padding: 1rem;
border-radius: 1rem;
gap: 7.5px;
cursor: pointer;
}
.taskContent {
display: flex;
flex-direction: column;
gap: 2px;
}
.taskTitle span {
font-weight: 600;
font-size: 18px;
line-height: 24px;
}
.taskPrize {
display: flex;
align-items: center;
gap: 5px;
}
.taskPrize span {
font-weight: 500;
font-size: 14px;
color: #BBBBBB;
line-height: 16px;
}
.claimPrize {
font-family: var(--font-manrope);
display: flex;
align-items: center;
justify-content: center;
background: #101010;
border: 1px solid #42BB47;
width: 267px;
height: 42px;
padding: 12px 16px;
border-radius: 40px;
gap: 5px;
color: #42BB47;
cursor: pointer;
}
.claimedPrize {
display: flex;
align-items: center;
gap: 5px;
}
.claimedPrize span {
color: #42BB47;
}
.taskClaimed {
background: #42BB4740;
cursor: auto;
}
.taskClaimedTitle span {
color: #42BB47;
}

View File

@@ -0,0 +1,64 @@
'use client';
import { FC, useEffect, useRef, useState } from 'react';
import styles from '@/components/tasks/Tasks.module.css';
import Task from '@/components/tasks/Task';
import { useTasksStore } from '@/store/tasks.state';
import { usePlayerStore } from '@/store/player.state';
interface ITasksProps {
handleClaimPrize: (taskId: number, prize: number) => Promise<void>;
handleOpenLink: (link: string, taskId: number) => Promise<void>;
}
const Tasks: FC<ITasksProps> = ({ handleOpenLink, handleClaimPrize }) => {
const tasksBlockRef = useRef<HTMLDivElement>(null);
const [isOverflow, setIsOverflow] = useState(false);
const { tasks } = useTasksStore();
const { player } = usePlayerStore();
useEffect(() => {
const checkOverflow = () => {
if (tasksBlockRef.current) {
setIsOverflow(
tasksBlockRef.current.scrollHeight > tasksBlockRef.current.clientHeight,
);
}
};
checkOverflow();
window.addEventListener('resize', checkOverflow);
return () => window.removeEventListener('resize', checkOverflow);
}, []);
return (
<div className={styles.tasks}>
<div className={styles.tasksHeader}>
<span>Задания</span>
</div>
<div
className={`${styles.tasksBlock} ${isOverflow ? styles.overflow : ''}`}
ref={tasksBlockRef}
>
{tasks.map(task => {
const finishedTask = player?.attributes.finished_tasks.data?.find(
el => el?.attributes.task?.data?.id === task?.id,
);
return (
<Task
key={task.id}
task={task}
finishedTask={finishedTask}
handleClaimPrize={handleClaimPrize}
handleOpenLink={handleOpenLink}
/>
);
})}
</div>
</div>
);
};
export default Tasks;

View File

@@ -0,0 +1,54 @@
'use client';
import Tasks from '@/components/tasks/Tasks';
import { useCallback } from 'react';
import { StatusTask } from '@/interfaces/task.type';
import { usePlayerStore } from '@/store/player.state';
import { useTokenStore } from '@/store/token.state';
import { PlayerApi } from '@/api/player.api';
const TasksContainer = () => {
const { player, setPlayer } = usePlayerStore();
const { token } = useTokenStore();
const handleClaimPrize = useCallback(
async (taskId: number) => {
if (!token) {
return;
}
const playerApi = PlayerApi.getInstance(token);
const updatedPlayer = await playerApi.updateTasks(player!.id, {
taskId: taskId,
status: 'claimed' as StatusTask,
});
const newPlayer = await playerApi.findPlayerByAuthId(
encodeURIComponent(updatedPlayer.attributes.auth_id),
);
setPlayer(newPlayer);
},
[player, token],
);
const handleOpenLink = useCallback(
async (link: string, taskId: number) => {
if (!token) {
return;
}
window.Telegram.WebApp.openLink(link);
const playerApi = PlayerApi.getInstance(token);
const updatedPlayer = await playerApi.updateTasks(player!.id, {
taskId: taskId,
status: 'pending_prize' as StatusTask,
});
const newPlayer = await playerApi.findPlayerByAuthId(
encodeURIComponent(updatedPlayer.attributes.auth_id),
);
setPlayer(newPlayer);
},
[player, token],
);
return <Tasks handleClaimPrize={handleClaimPrize} handleOpenLink={handleOpenLink} />;
};
export default TasksContainer;