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,13 @@
#Strapi
NEXT_PUBLIC_STRAPI_USER="admin"
NEXT_PUBLIC_STRAPI_PASSWORD="Admin1234"
#Prod
#NEXT_PUBLIC_STRAPI_URL="https://strapi.rbx-click.ru/api"
#NEXT_PUBLIC_STRAPI="https://strapi.rbx-click.ru"
#NEXT_PUBLIC_BOT_API="https://bot.rbx-click.ru"
#Local
NEXT_PUBLIC_STRAPI_URL="/api/strapi"
NEXT_PUBLIC_STRAPI=""
NEXT_PUBLIC_BOT_API="/api/bot"

View File

@@ -0,0 +1,24 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"prettier/prettier": ["warn", {
"endOfLine": "auto",
"semi": true,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "all",
"useTabs": false,
"tabWidth": 4,
"jsxSingleQuote": true,
"bracketSpacing": true,
"arrowParens": "avoid"
}
],
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "off",
"prefer-const": "warn",
"no-console": "warn"
}
}

View File

@@ -0,0 +1,27 @@
name: Deploy to prod
on:
workflow_dispatch:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy with pm2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{secrets.PROD_SSH_HOST}}
username: ${{secrets.PROD_SSH_USERNAME}}
password: ${{secrets.PROD_SSH_PASSWORD}}
script_stop: true
script: |
export NVM_DIR=~/.nvm
source ~/.nvm/nvm.sh
cd ${{secrets.PROD_PROJECT_PATH}}
git checkout main
git pull
yarn install
pm2 restart ecosystem.config.js

38
CMyTapper/robucks-front/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.idea

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -0,0 +1,40 @@
import { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
import { IPlayer } from '@/interfaces/player.type';
import { IExchangeRequest } from '@/api/exchange-request.api';
export interface IArchiveRequest {
id: number;
attributes: {
player: IPlayer;
exchange_requests: { data: IExchangeRequest[] };
rejected_requests: { data: IExchangeRequest[] };
};
}
export class ArchiveRequestApi {
private static instance: ArchiveRequestApi | null = null;
private httpInstance: AxiosInstance;
private constructor(token: string) {
this.httpInstance = HttpInstanceFactory.updateAuthorizedInstance(token);
}
public static getInstance(token: string): ArchiveRequestApi {
if (this.instance) return this.instance;
this.instance = new ArchiveRequestApi(token);
return this.instance;
}
async createArchiveRequest(playerId: number): Promise<IArchiveRequest> {
return (
await this.httpInstance.post<IArchiveRequest>(`/archive-requests`, {
data: {
player: {
connect: [{ id: playerId }],
},
},
})
).data;
}
}

View File

@@ -0,0 +1,34 @@
import { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '../utils/HttpInstanceFactory';
export interface IUser {
jwt: string;
user: {
id: number;
username: string;
confirmed: boolean;
blocked: boolean;
}
}
export class AuthApi {
private static instance: AuthApi | null = null;
private httpInstance: AxiosInstance;
private constructor() {
this.httpInstance = HttpInstanceFactory.getBaseInstance();
}
public static getInstance(): AuthApi {
if (this.instance) return this.instance;
this.instance = new AuthApi();
return this.instance;
}
async login(): Promise<IUser> {
return (await this.httpInstance.post<IUser>(`/auth/local`, {
identifier: process.env.NEXT_PUBLIC_STRAPI_USER,
password: process.env.NEXT_PUBLIC_STRAPI_PASSWORD,
})).data;
}
}

View File

@@ -0,0 +1,42 @@
import axios, { AxiosInstance } from 'axios';
export interface IConfigBot {
token: string;
dateLifetimeJwt: Date;
}
export interface IBotLink {
link: string;
}
export class ConfigBotApi {
private static instance: ConfigBotApi | null = null;
private httpBotInstance: AxiosInstance;
private constructor() {
const isServer = typeof window === 'undefined';
const baseURL = isServer
? 'http://127.0.0.1:3001'
: process.env.NEXT_PUBLIC_BOT_API;
this.httpBotInstance = axios.create({
baseURL,
headers: {
'ngrok-skip-browser-warning': 'true',
},
});
}
public static getInstance(): ConfigBotApi {
if (this.instance) return this.instance;
this.instance = new ConfigBotApi();
return this.instance;
}
async getConfigBot(): Promise<IConfigBot> {
return (await this.httpBotInstance.get<IConfigBot>(`/config-bot-data`)).data;
}
async getBotLink(): Promise<IBotLink> {
return (await this.httpBotInstance.get<IBotLink>(`/link-bot`)).data;
}
}

View File

@@ -0,0 +1,33 @@
import { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
import { IPlayer } from '@/interfaces/player.type';
export interface IConfig {
id: number;
attributes: {
farm_amount: number;
farming_time: number;
admins: { data: IPlayer[] };
};
}
export class ConfigApi {
private static instance: ConfigApi | null = null;
private httpInstance: AxiosInstance;
private constructor(token: string, shouldUpdateInstance: boolean = false) {
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
}
public static getInstance(token: string, shouldUpdateInstance: boolean = false): ConfigApi {
if (!shouldUpdateInstance && this.instance) {
return this.instance;
}
this.instance = new ConfigApi(token);
return this.instance;
}
async getConfig(): Promise<{ data: IConfig }> {
return (await this.httpInstance.get<{ data: IConfig }>(`/config?populate=*`)).data;
}
}

View File

@@ -0,0 +1,82 @@
import axios, { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
import { IPlayer } from '@/interfaces/player.type';
import { toFixed } from '@/utils/FormatNumber';
export interface IExchangeCreate {
amount: number;
total: number;
playerId: number;
nickname: string;
balance: number;
telegram_id: string[] | number[];
shop_item?: number;
archive_request_id: number;
}
export interface IExchangeRequest {
id: number;
attributes: {
amount: number;
total: number;
player: { data: IPlayer };
roblox_nick: string;
status: 'done' | 'rejected' | 'pending';
};
}
export class ExchangeRequestApi {
private static instance: ExchangeRequestApi | null = null;
private httpInstance: AxiosInstance;
private httpBotInstance: AxiosInstance;
private constructor(token: string) {
this.httpInstance = HttpInstanceFactory.updateAuthorizedInstance(token);
this.httpBotInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_BOT_API,
headers: {
'Content-Type': 'application/json',
},
});
}
public static getInstance(token: string): ExchangeRequestApi {
if (this.instance) return this.instance;
this.instance = new ExchangeRequestApi(token);
return this.instance;
}
async createExchange(data: IExchangeCreate): Promise<IExchangeRequest> {
const res = (
await this.httpInstance.post<{ data: IExchangeRequest }>(
`/exchange-requests?populate=*`,
{
data: {
amount: toFixed(data.amount),
total: toFixed(data.total),
roblox_nick: data.nickname,
status: 'pending',
shop_item: data.shop_item ? data.shop_item : null,
},
},
)
).data.data;
if (res) {
await this.httpInstance.put(`/archive-requests/${data.archive_request_id}`, {
data: {
exchange_requests: {
connect: [res.id],
},
},
});
await this.httpInstance.put(`/players/${data.playerId}`, {
data: { balance: toFixed(data.balance - data.amount) },
});
await this.httpBotInstance.post(`/notify`, {
telegram_id: data.telegram_id,
exchange_id: res.id,
});
}
return res;
}
}

View File

@@ -0,0 +1,56 @@
import { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
import { IPlayer, IUpdatePlayerTask } from '@/interfaces/player.type';
export class PlayerApi {
private static instance: PlayerApi | null = null;
private httpInstance: AxiosInstance;
private populateString: string =
'populate=archive_request.rejected_requests&populate=archive_request.exchange_requests&populate=finished_tasks.task.icon&populate=referral.my_referrals&populate=my_referrals.my_referrals';
private constructor(token?: string) {
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
}
public static getInstance(token?: string): PlayerApi {
if (this.instance) return this.instance;
this.instance = new PlayerApi(token);
return this.instance;
}
async findPlayerByAuthId(authId: string): Promise<IPlayer | null> {
const filteredPlayers: IPlayer[] = (
await this.httpInstance.get(
`/players/?filters[auth_id]=${authId}&${this.populateString}`,
)
).data.data;
return filteredPlayers.length > 0 ? filteredPlayers[0] : null;
}
async startFarm(startFarm: Date, playerId: number): Promise<IPlayer> {
return (
await this.httpInstance.put<{ data: IPlayer }>(
`/players/${playerId}?${this.populateString}`,
{
data: { start_farm: startFarm },
},
)
).data.data;
}
async claimRewards(playerId: number, balance: number): Promise<IPlayer> {
return (
await this.httpInstance.put<{ data: IPlayer }>(
`/players/${playerId}?${this.populateString}`,
{
data: { balance: balance, start_farm: null },
},
)
).data.data;
}
async updateTasks(playerId: number, data: IUpdatePlayerTask): Promise<IPlayer> {
return (await this.httpInstance.put<IPlayer>(`/players/finished-tasks/${playerId}`, data))
.data;
}
}

View File

@@ -0,0 +1,39 @@
import { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
export interface IShopItem {
id: number;
attributes: {
isDecimal: boolean;
name: string;
price: number;
icon: {
data: {
id: number;
attributes: {
url: string;
};
};
};
};
}
export class ShopItemApi {
private static instance: ShopItemApi | null = null;
private httpInstance: AxiosInstance;
private constructor(token?: string) {
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
}
public static getInstance(token?: string): ShopItemApi {
if (this.instance) return this.instance;
this.instance = new ShopItemApi(token);
return this.instance;
}
async getAllShopItems(): Promise<IShopItem[]> {
return (await this.httpInstance.get<{ data: IShopItem[] }>(`/shop-items?populate=*`)).data
.data;
}
}

View File

@@ -0,0 +1,38 @@
import { AxiosInstance } from 'axios';
import { HttpInstanceFactory } from '@/utils/HttpInstanceFactory';
export interface ITask {
id: number;
attributes: {
title: string;
link: string;
prize: number;
icon: {
data: {
id: number;
attributes: {
url: string;
};
};
};
};
}
export class TasksApi {
private static instance: TasksApi | null = null;
private httpInstance: AxiosInstance;
private constructor(token?: string) {
this.httpInstance = HttpInstanceFactory.getAuthorizedInstance(token);
}
public static getInstance(token?: string): TasksApi {
if (this.instance) return this.instance;
this.instance = new TasksApi(token);
return this.instance;
}
async getAllTasks(): Promise<ITask[]> {
return (await this.httpInstance.get<{ data: ITask[] }>(`/tasks?populate=*`)).data.data;
}
}

View File

@@ -0,0 +1,10 @@
'use server';
import { cookies } from 'next/headers';
export async function setNewToken(token: string) {
cookies().set('token', token);
}
export async function setNowDate() {
cookies().set('dateLifetimeJwt', new Date().toDateString());
}

View File

@@ -0,0 +1,11 @@
import FriendsContainer from '@/components/friends/FriendsContiner';
import { ConfigBotApi } from '@/api/config-bot.api';
const page = async () => {
const configBotApi = ConfigBotApi.getInstance();
const botLink = await configBotApi.getBotLink();
return <FriendsContainer botLink={botLink.link} />;
};
export default page;

View File

@@ -0,0 +1,7 @@
import HomeContainer from '@/components/home/HomeContainer';
const page = async () => {
return <HomeContainer />;
};
export default page;

View File

@@ -0,0 +1,13 @@
import Wrapper from '@/components/ui/wrapper/Wrapper';
import { Providers } from '@/components/providers/Providers';
import { PropsWithChildren } from 'react';
import { Params } from 'next/dist/shared/lib/router/utils/route-matcher';
export default function RootLayout({ children, params }: PropsWithChildren & { params: Params }) {
const id = decodeURI(params.id);
return (
<Wrapper>
<Providers id={id}>{children}</Providers>
</Wrapper>
);
}

View File

@@ -0,0 +1,5 @@
const page = async () => {
return <>hello</>;
};
export default page;

View File

@@ -0,0 +1,7 @@
import ShopContainer from '@/components/shop/ShopContainer';
const page = async () => {
return <ShopContainer />;
};
export default page;

View File

@@ -0,0 +1,7 @@
import TasksContainer from '@/components/tasks/TasksContainer';
const page = async () => {
return <TasksContainer />;
};
export default page;

View File

@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from 'next/server';
const BOT_API_URL = process.env.BOT_API_INTERNAL || 'http://127.0.0.1:3001';
async function proxyRequest(req: NextRequest, params: { path: string[] }) {
const path = params.path.join('/');
const url = `${BOT_API_URL}/${path}`;
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
const fetchOptions: RequestInit = {
method: req.method,
headers,
};
if (req.method !== 'GET' && req.method !== 'HEAD') {
const body = await req.text();
if (body) {
fetchOptions.body = body;
}
}
const response = await fetch(url, fetchOptions);
const data = await response.json();
return NextResponse.json(data, { status: response.status });
} catch (error) {
console.error('Bot proxy error:', error);
return NextResponse.json(
{ error: 'Bot API unavailable' },
{ status: 502 }
);
}
}
export async function GET(
req: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(req, params);
}
export async function POST(
req: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(req, params);
}

View File

@@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from 'next/server';
const STRAPI_URL = process.env.STRAPI_INTERNAL || 'http://127.0.0.1:1337/api';
async function proxyRequest(req: NextRequest, params: { path: string[] }) {
const path = params.path.join('/');
const searchParams = req.nextUrl.searchParams.toString();
const url = `${STRAPI_URL}/${path}${searchParams ? `?${searchParams}` : ''}`;
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
const authHeader = req.headers.get('Authorization');
if (authHeader) {
headers['Authorization'] = authHeader;
}
const fetchOptions: RequestInit = {
method: req.method,
headers,
};
if (req.method !== 'GET' && req.method !== 'HEAD') {
const body = await req.text();
if (body) {
fetchOptions.body = body;
}
}
const response = await fetch(url, fetchOptions);
const data = await response.json();
return NextResponse.json(data, { status: response.status });
} catch (error) {
console.error('Strapi proxy error:', error);
return NextResponse.json(
{ error: 'Strapi API unavailable' },
{ status: 502 }
);
}
}
export async function GET(
req: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(req, params);
}
export async function POST(
req: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(req, params);
}
export async function PUT(
req: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(req, params);
}
export async function DELETE(
req: NextRequest,
{ params }: { params: { path: string[] } }
) {
return proxyRequest(req, params);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,78 @@
import type { Metadata } from 'next';
import Script from 'next/script';
import localFont from 'next/font/local';
import { Manrope } from 'next/font/google';
import '../public/globals.css';
import { PropsWithChildren } from 'react';
import { Params } from 'next/dist/shared/lib/router/utils/route-matcher';
const manrope = Manrope({
weight: ['200', '300', '400', '500', '600', '700'],
style: ['normal'],
subsets: ['latin'],
variable: '--font-manrope',
});
const clashFont = localFont({
src: [
{
path: '../public/fonts/ClashDisplay-Bold.otf',
weight: '800',
style: 'normal',
},
{
path: '../public/fonts/ClashDisplay-Extralight.otf',
weight: '100',
style: 'normal',
},
{
path: '../public/fonts/ClashDisplay-Light.otf',
weight: '200',
style: 'normal',
},
{
path: '../public/fonts/ClashDisplay-Regular.otf',
weight: '400',
style: 'normal',
},
{
path: '../public/fonts/ClashDisplay-Semibold.otf',
weight: '600',
style: 'normal',
},
{
path: '../public/fonts/ClashDisplay-Medium.otf',
weight: '500',
style: 'normal',
},
],
variable: '--font-clash',
});
export const metadata: Metadata = {
title: 'Robucks clicker',
description: 'Robucks clicker',
};
export default async function RootLayout({
children,
params,
}: PropsWithChildren & { params: Params }) {
return (
<html lang='ru'>
<head>
<Script
src='https://telegram.org/js/telegram-web-app.js'
strategy='beforeInteractive'
/>
</head>
<body
className={`${clashFont.className} ${manrope.variable}`}
suppressHydrationWarning={true}
>
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <span>Not found</span>;
}

View File

@@ -0,0 +1,34 @@
.friend {
display: flex;
align-items: center;
justify-content: space-between;
background: #FFFFFF0D;
border-radius: 1rem;
padding: 1rem;
height: 74px;
}
.friendContent {
display: flex;
align-items: center;
gap: 12px;
}
.friendData {
display: flex;
flex-direction: column;
}
.friendReferrals {
display: flex;
align-items: center;
gap: 3px;
}
.friendBalance span {
font-family: var(--font-manrope);
font-weight: 600;
font-size: 18px;
line-height: 24px;
color: white;
}

View File

@@ -0,0 +1,43 @@
'use client';
import styles from '@/components/friends/Friend.module.css';
import Image from 'next/image';
import { IPlayer } from '@/interfaces/player.type';
import { toFixed } from '@/utils/FormatNumber';
type Props = {
player: IPlayer;
};
const Friend = ({ player }: Props) => {
return (
<div className={styles.friend}>
<div className={styles.friendContent}>
<div className={styles.friendAvatar}>
<Image
src={'/icons/friend-icon.svg'}
alt={'friend avatar'}
height={42}
width={42}
/>
</div>
<div className={styles.friendData}>
<span>{player?.attributes.telegram_nick}</span>
<div className={styles.friendReferrals}>
<Image
src={'/icons/referrals-icon.svg'}
alt={'referrals icon'}
width={14}
height={14}
/>
<span>{player?.attributes.my_referrals?.data?.length || 0}</span>
</div>
</div>
</div>
<div className={styles.friendBalance}>
<span>{toFixed(player?.attributes.balance)}</span>
</div>
</div>
);
};
export default Friend;

View File

@@ -0,0 +1,113 @@
.friends {
background: #101010;
height: 100vh;
overflow-y: auto;
padding-bottom: 90px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.friendsTitle {
align-items: flex-start;
padding-top: 45px;
padding-left: 1.5rem;
font-weight: 600;
font-size: 19px;
color: white;
line-height: 21px;
font-family: var(--font-manrope);
}
.inviteBlock {
width: min(343px, 100%);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-top: 1rem;
padding: 20px 16px;
border-radius: 1.5rem;
background: linear-gradient(81.19deg, rgba(66, 187, 71, 0.06) 0%, rgba(66, 187, 71, 0.2) 100%), #0E0E10;
border: 1px solid #18181A;
}
.rewardInvite {
display: flex;
align-items: center;
flex-direction: row-reverse;
gap: 3px;
font-family: var(--font-clash);
}
.rewardInvite span {
font-size: 42px;
font-weight: 600;
line-height: 51px;
background: linear-gradient(180deg, #42BB47 4.41%, #33A137 61.59%, #42BB47 101.32%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.inviteButtonBlock {
width: 100%;
}
.inviteButtonBlock button {
width: 100%;
height: 46px;
border-radius: 40px;
color: white;
background: #42BB47;
border: none;
font-weight: 600;
font-size: 16px;
line-height: 21px;
font-family: var(--font-manrope);
cursor: pointer;
}
.infoInvite {
text-align: center;
font-family: var(--font-manrope);
}
.infoInvite span {
font-size: 12px;
font-weight: 600;
line-height: 16px;
color: #BBBBBB;
}
.friendsList {
display: flex;
flex-direction: column;
padding-top: 1rem;
margin-top: 7px;
gap: 5px;
padding-bottom: 7px;
}
.friendsListBlock {
width: min(375px, 100%);
margin-top: 2rem;
background: #0E0E10;
padding: 20px 16px;
border: 1px solid #18181A;
border-radius: 1.5rem;
}
.friendsListHeader {
display: flex;
justify-content: space-between;
font-size: 19px;
color: white;
font-weight: 600;
font-family: var(--font-manrope);
line-height: 19px;
}

View File

@@ -0,0 +1,67 @@
'use client';
import styles from '@/components/friends/Friends.module.css';
import Image from 'next/image';
import { FC } from 'react';
import Friend from '@/components/friends/Friend';
import CopyLinkToast from '@/components/ui/copy-link-toast/CopyLinkToast';
import { usePlayerStore } from '@/store/player.state';
interface IFriends {
isVisible: boolean;
referral_fee: number | undefined;
friendsAndReferralsCount: { friendsWord: string; referralWord: string };
handleCopyInviteLink: () => Promise<void>;
handleCloseToastLink: () => void;
}
const Friends: FC<IFriends> = ({
isVisible,
referral_fee,
friendsAndReferralsCount,
handleCopyInviteLink,
handleCloseToastLink,
}) => {
const { player } = usePlayerStore();
const referrals = player?.attributes.my_referrals.data;
return (
<div className={styles.friends}>
<div className={styles.friendsTitle}>
<span>Друзья</span>
</div>
<div className={styles.content}>
<div className={styles.inviteBlock}>
<div className={styles.rewardInvite}>
<span>{referral_fee || 0}</span>
<Image
src={'/icons/robucks-icon.svg'}
alt={'robucks icon'}
width={32}
height={32}
/>
</div>
<div className={styles.inviteButtonBlock}>
<button onClick={handleCopyInviteLink}>Пригласи друга</button>
</div>
<div className={styles.infoInvite}>
<span>
Получай 10% от твоих друзей +5% от друзей реферала +2.5% от их рефералов
</span>
</div>
</div>
<div className={styles.friendsListBlock}>
<div className={styles.friendsListHeader}>
<span>{friendsAndReferralsCount.friendsWord}</span>
<span>{friendsAndReferralsCount.referralWord}</span>
</div>
<div className={styles.friendsList}>
{referrals?.map(referral => <Friend key={referral.id} player={referral} />)}
</div>
</div>
<CopyLinkToast isVisible={isVisible} handleCloseToastLink={handleCloseToastLink} />
</div>
</div>
);
};
export default Friends;

View File

@@ -0,0 +1,50 @@
'use client';
import Friends from '@/components/friends/Friends';
import { useCallback, useEffect, useState } from 'react';
import { CalculateReferralsCount } from '@/utils/CalculateReferralsCount';
import { updateFriendAndReferralCounts } from '@/utils/UpdateFriendAndReferralCount';
import { usePlayerStore } from '@/store/player.state';
const FriendsContainer = ({ botLink }: { botLink: string }) => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const [userData, setUserData] = useState<{ id: number; username: string } | null>(null);
const { player } = usePlayerStore();
useEffect(() => {
const initDataUnsafe = window.Telegram?.WebApp?.initDataUnsafe;
if (!initDataUnsafe?.user) {
return;
}
setUserData(initDataUnsafe.user);
}, []);
const handleCopyInviteLink = useCallback(async () => {
setIsVisible(true);
setTimeout(() => {
setIsVisible(false);
}, 3000);
return await navigator.clipboard.writeText(`${botLink}ref_${userData?.id}`);
}, [userData, botLink]);
const handleCloseToastLink = useCallback(() => {
setIsVisible(false);
}, []);
return (
<Friends
isVisible={isVisible}
referral_fee={player?.attributes.referral_fee}
friendsAndReferralsCount={updateFriendAndReferralCounts(
player?.attributes.my_referrals?.data?.length || 0,
CalculateReferralsCount(player),
)}
handleCopyInviteLink={handleCopyInviteLink}
handleCloseToastLink={handleCloseToastLink}
/>
);
};
export default FriendsContainer;

View File

@@ -0,0 +1,137 @@
.home {
height: 100vh;
width: 100%;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
background-image: url("../../public/images/background-img.jpg");
}
.main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
padding-bottom: 3rem;
height: 100%;
gap: 25px;
}
.greeting {
display: flex;
align-items: center;
font-size: 28px;
font-weight: 600;
line-height: 34.4px;
}
.greeting span {
background: linear-gradient(45deg, #42BB47, #33A137, #42BB47);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.balance {
display: flex;
flex-direction: column;
align-items: center;
}
.balanceAmount {
display: flex;
align-items: center;
gap: 7px;
}
.totalBalance {
font-size: 42px;
font-weight: 600;
line-height: 51px;
background: linear-gradient(45deg, #42BB47, #33A137, #42BB47);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.miningButton {
margin-bottom: 75px;
width: 187px;
height: 60px;
padding: 20px 30px;
background: #42BB47;
color: white;
font-weight: 600;
line-height: 19.6px;
box-shadow: 0 0 42px 0 #42BB47B2;
border-radius: 30px;
border: 0.5px solid;
cursor: pointer;
border-image-source: linear-gradient(180deg, #42BB47 4.41%, #308A34 61.59%, #42BB47 101.32%);
text-decoration: none;
}
.farmingButton {
margin-bottom: 75px;
width: 300px;
height: 60px;
padding: 20px 30px;
background: black;
box-shadow: 0px 0px 42px 0px #42BB4780;
text-decoration: none;
cursor: not-allowed;
border-radius: 30px;
border: 1px solid #42BB4780;
color: #42BB4780;
font-weight: 600;
line-height: 19.6px;
}
.claimReward {
box-shadow: 0px 0px 42px 0px #42BB4780;
width: 257px;
height: 60px;
padding: 20px 30px;
cursor: pointer;
margin-bottom: 75px;
border-radius: 30px;
text-decoration: none;
font-weight: 600;
line-height: 19.6px;
background: #10101080;
border: 1px solid #42BB47;
color: #42BB4780;
}
.claimAmount {
display: flex;
align-items: center;
gap: 5px;
}
.claimAmount span {
font-size: 18px;
font-weight: 600;
color: #42BB47;
}
.reward {
display: flex;
align-content: center;
gap: 10px;
color: #42BB47;
}
.reward span {
color: #42BB47;
}
.disabledButton {
background: #10101080;
color: #42BB47;
cursor: not-allowed;
}
.miningButton,
.farmingButton,
.claimReward {
transition: all 0.2s ease-in-out;
}

View File

@@ -0,0 +1,62 @@
'use client';
import React, { FC } from 'react';
import styles from './Home.module.css';
import Image from 'next/image';
import { IConfig } from '@/api/config.api';
import ButtonMining from '@/components/ui/button-mining/ButtonMining';
import { usePlayerStore } from '@/store/player.state';
import { toFixed } from '@/utils/FormatNumber';
interface IHome {
remainingTime: string | null;
config: IConfig | null;
isLoading: boolean;
typeButton: 'pending' | 'farming' | 'claim_reward';
handleStartFarming: () => void;
handleClaimRewards: () => void;
}
const Home: FC<IHome> = ({
remainingTime,
config,
isLoading,
typeButton,
handleStartFarming,
handleClaimRewards,
}) => {
const { player } = usePlayerStore();
return (
<div className={styles.home}>
<div className={styles.main}>
<div className={styles.greeting}>
<p>Привет, </p>
<span>{player ? player.attributes.telegram_nick : '-'}!</span>
</div>
<div className={styles.balance}>
<span>Ваш баланс</span>
<div className={styles.balanceAmount}>
<Image
src={'/icons/robucks-icon.svg'}
alt={'robucks icon'}
width={32}
height={32}
/>
<span className={styles.totalBalance}>
{player ? toFixed(player.attributes.balance) : '0'}
</span>
</div>
</div>
<ButtonMining
isLoading={isLoading}
typeButton={typeButton}
remainingTime={remainingTime}
farm_amount={config?.attributes.farm_amount}
handleStartFarming={handleStartFarming}
handleClaimRewards={handleClaimRewards}
/>
</div>
</div>
);
};
export default Home;

View File

@@ -0,0 +1,93 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import Home from '@/components/home/Home';
import { FormatTime } from '@/utils/FormatTime';
import { useButtonStore } from '@/store/button.state';
import { PlayerApi } from '@/api/player.api';
import { usePlayerStore } from '@/store/player.state';
import { useConfigStore } from '@/store/config.state';
import { useTokenStore } from '@/store/token.state';
import { toFixed } from '@/utils/FormatNumber';
const HomeContainer = () => {
const { typeButton, setTypeButton } = useButtonStore();
const { player: playerStore, setPlayer } = usePlayerStore();
const { config: configStore } = useConfigStore();
const { token } = useTokenStore();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [remainingTime, setRemainingTime] = useState<number | null>(null);
useEffect(() => {
if (isLoading) {
setIsLoading(false);
}
}, [isLoading]);
useEffect(() => {
if (playerStore && playerStore.attributes.start_farm && configStore) {
const farmingTime = configStore.attributes.farming_time * 1000;
const endTime = new Date(playerStore.attributes.start_farm).getTime() + farmingTime;
const updateRemainingTime = () => {
const now = new Date().getTime();
const timeLeft = endTime - now;
setRemainingTime(timeLeft > 0 ? timeLeft : 0);
};
updateRemainingTime();
const interval = setInterval(updateRemainingTime, 1000);
return () => clearInterval(interval);
} else {
setRemainingTime(null);
}
}, [playerStore, configStore]);
useEffect(() => {
if (!remainingTime && !playerStore?.attributes.start_farm) {
setTypeButton('pending');
} else if (remainingTime && playerStore?.attributes.start_farm) {
setTypeButton('farming');
} else if (!remainingTime && playerStore?.attributes.start_farm) {
setTypeButton('claim_reward');
}
}, [remainingTime, playerStore]);
const handleStartFarming = useCallback(async () => {
if (!token || !playerStore) {
return;
}
const api = PlayerApi.getInstance(token);
const updatedPlayer = await api.startFarm(new Date(), playerStore.id);
setPlayer(updatedPlayer);
}, [playerStore]);
const handleClaimRewards = useCallback(async () => {
if (!token || !playerStore || !configStore) {
return;
}
const api = PlayerApi.getInstance(token);
// TODO: улучшить безопасность в методе claimRewards
const updatedPlayer = await api.claimRewards(
playerStore.id,
toFixed(playerStore?.attributes.balance) + configStore?.attributes.farm_amount,
);
setPlayer(updatedPlayer);
}, [playerStore, configStore]);
return (
<Home
config={configStore}
isLoading={isLoading}
typeButton={typeButton}
remainingTime={FormatTime(remainingTime)}
handleStartFarming={handleStartFarming}
handleClaimRewards={handleClaimRewards}
/>
);
};
export default HomeContainer;

View File

@@ -0,0 +1,21 @@
'use client';
import { PropsWithChildren, useEffect } from 'react';
import { useTokenStore } from '@/store/token.state';
import { ConfigApi } from '@/api/config.api';
import { useConfigStore } from '@/store/config.state';
export const ConfigProvider = ({ children }: PropsWithChildren) => {
const { setConfig } = useConfigStore();
const { token } = useTokenStore();
useEffect(() => {
if (!token) {
return;
}
const api = ConfigApi.getInstance(token);
api.getConfig().then(config => setConfig(config.data));
}, [token]);
return <>{children}</>;
};

View File

@@ -0,0 +1,72 @@
'use client';
import { PropsWithChildren, useEffect, useState } from 'react';
import { usePlayerStore } from '@/store/player.state';
import { ProvidersProps } from '@/components/providers/Providers';
import { useTokenStore } from '@/store/token.state';
import { PlayerApi } from '@/api/player.api';
import styles from './Providers.module.css';
export const PlayerProvider = ({
children,
id,
}: PropsWithChildren & Pick<ProvidersProps, 'id'>) => {
const { player, setPlayer, isLoading, stopLoading } = usePlayerStore();
const { token } = useTokenStore();
const [username, setUsername] = useState('');
useEffect(() => {
if (!token) {
console.log('[PlayerProvider] no token yet, waiting...');
return;
}
console.log('[PlayerProvider] token received, loading player...');
try {
const tgUser = window.Telegram?.WebApp?.initDataUnsafe?.user;
const username = String(tgUser?.username || 'unknown');
console.log('[PlayerProvider] telegram username:', username, 'tgUser:', tgUser);
setUsername(username);
} catch (e) {
console.error('[PlayerProvider] error getting telegram user:', e);
}
const playerApi = PlayerApi.getInstance(token);
playerApi.findPlayerByAuthId(id).then(player => {
console.log('[PlayerProvider] player loaded:', player?.id);
setPlayer(player);
stopLoading();
}).catch(err => {
console.error('[PlayerProvider] error loading player:', err);
stopLoading();
});
}, [token]);
if (isLoading) {
return (
<div className={styles.loading}>
<span>Загрузка...</span>
</div>
);
}
if (!player) {
return (
<div className={styles.loading}>
<span>
Что-то пошло не так... Попробуйте написать /start, чтобы получить актуальную
ссылку
</span>
</div>
);
}
if (player.attributes.telegram_nick !== username) {
return (
<div className={styles.loading}>
<span>
Похоже, что Вы изменили свой ID в Telegram, попробуйте написать /start, чтобы
получить актуальную ссылку
</span>
</div>
);
}
return <>{children}</>;
};

View File

@@ -0,0 +1,10 @@
.loading {
width: 100vw;
height: 100vh;
background-color: black;
display: flex;
justify-content: center;
align-items: center;
font-family: var(--font-manrope);
text-align: center;
}

View File

@@ -0,0 +1,28 @@
import { PropsWithChildren } from 'react';
import { PlayerProvider } from '@/components/providers/PlayerProvider';
import { TokenProvider } from '@/components/providers/TokenProvider';
import { TasksProvider } from '@/components/providers/TasksProvider';
import { ShopItemsProviver } from '@/components/providers/ShopItemsProvider';
import { ConfigProvider } from '@/components/providers/ConfigProvider';
import Navigation from '@/components/ui/navigation/Navigation';
export type ProvidersProps = {
id: string;
};
export const Providers = ({ children, id }: PropsWithChildren & ProvidersProps) => {
return (
<TokenProvider>
<TasksProvider>
<ShopItemsProviver>
<ConfigProvider>
<PlayerProvider id={id}>
{children}
<Navigation id={id} />
</PlayerProvider>
</ConfigProvider>
</ShopItemsProviver>
</TasksProvider>
</TokenProvider>
);
};

View File

@@ -0,0 +1,21 @@
'use client';
import { PropsWithChildren, useEffect } from 'react';
import { useTokenStore } from '@/store/token.state';
import { ShopItemApi } from '@/api/shop-item.api';
import { useShopItemsStore } from '@/store/shopItems.state';
export const ShopItemsProviver = ({ children }: PropsWithChildren) => {
const { setShopItems } = useShopItemsStore();
const { token } = useTokenStore();
useEffect(() => {
if (!token) {
return;
}
const api = ShopItemApi.getInstance(token);
api.getAllShopItems().then(shopItems => setShopItems(shopItems));
}, [token]);
return <>{children}</>;
};

View File

@@ -0,0 +1,21 @@
'use client';
import { PropsWithChildren, useEffect } from 'react';
import { useTokenStore } from '@/store/token.state';
import { useTasksStore } from '@/store/tasks.state';
import { TasksApi } from '@/api/tasks.api';
export const TasksProvider = ({ children }: PropsWithChildren) => {
const { setTasks } = useTasksStore();
const { token } = useTokenStore();
useEffect(() => {
if (!token) {
return;
}
const api = TasksApi.getInstance(token);
api.getAllTasks().then(tasks => setTasks(tasks));
}, [token]);
return <>{children}</>;
};

View File

@@ -0,0 +1,22 @@
'use client';
import { PropsWithChildren, useEffect } from 'react';
import { ConfigBotApi } from '@/api/config-bot.api';
import { useTokenStore } from '@/store/token.state';
const configBotApi = ConfigBotApi.getInstance();
export const TokenProvider = ({ children }: PropsWithChildren) => {
const { setToken } = useTokenStore();
useEffect(() => {
configBotApi.getConfigBot().then(cfg => {
console.log('[TokenProvider] got config:', cfg);
setToken(cfg.token);
}).catch(err => {
console.error('[TokenProvider] error getting config:', err);
});
}, []);
return <>{children}</>;
};

View File

@@ -0,0 +1,155 @@
.shop {
background: #101010;
height: calc(100vh - 82px);
font-family: var(--font-manrope);
overflow-y: auto;
padding-bottom: 82px;
}
.item_img {
--size: 32px;
width: var(--size);
height: var(--size);
max-width: var(--size);
max-height: var(--size);
}
.item_img_sm {
--size: 18px;
width: var(--size);
height: var(--size);
max-width: var(--size);
max-height: var(--size);
}
.item_img > img, .item_img_sm > img {
width: 100%;
height: 100%;
}
.content {
padding-top: 45px;
padding-left: 1rem;
padding-right: 1rem;
}
.shop.overflow {
padding-bottom: 90px;
}
.header {
padding-bottom: 1rem;
}
.itemsAll {
color: #42BB47;
font-weight: 500;
font-size: 16px;
line-height: 22px;
}
.header span {
color: white;
font-weight: 600;
font-size: 19px;
line-height: 20px;
}
.exchangeRobucks {
display: flex;
align-items: center;
gap: 4px;
}
.items {
min-height: 155px;
border-radius: 1.5rem;
padding: 20px 16px;
background: #0E0E10;
display: flex;
flex-direction: column;
gap: 8px;
}
.itemsHeader {
display: flex;
justify-content: space-between;
}
.item {
cursor: pointer;
background: #FFFFFF0D;
height: 76px;
border-radius: 1rem;
display: flex;
align-items: center;
padding: 1rem;
gap: 8px;
}
.shopItem {
cursor: pointer;
background: #FFFFFF0D;
height: 76px;
border-radius: 1rem;
display: flex;
align-items: center;
padding: 1rem;
gap: 8px;
}
.shopItemPrice {
display: flex;
align-items: center;
gap: 8px;
}
.itemInfo {
display: flex;
flex-direction: column;
}
.itemInfoTitle span {
font-weight: 600;
font-size: 18px;
line-height: 24px;
}
.itemInfoContent {
display: flex;
align-items: center;
gap: 5px;
}
.upgrades {
margin-top: 1rem;
border-radius: 1.5rem;
background: #0E0E10;
border: 1px solid #18181A;
padding: 20px 16px;
}
.upgradesTitle span {
color: white;
font-weight: 600;
font-size: 19px;
line-height: 20.9px;
}
.upgradesContent {
padding-top: 1rem;
}
.upgradesContent span {
font-weight: 500;
color: white;
font-size: 16px;
line-height: 21px;
}
.shopItems {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 0.8rem;
}

View File

@@ -0,0 +1,84 @@
'use client';
import styles from '@/components/shop/Shop.module.css';
import { FC, useEffect, useRef, useState } from 'react';
import ModalExchangeContainer from '@/components/ui/modal-exchange/ModalExchangeContainer';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import ShopItem from '@/components/shop/ShopItem';
import { IHandleOpen } from '@/components/shop/ShopContainer';
import { useShopItemsStore } from '@/store/shopItems.state';
import { usePlayerStore } from '@/store/player.state';
import { toFixed } from '@/utils/FormatNumber';
interface IShop {
isOpen: boolean;
handleClose: () => void;
handleOpen: (data: IHandleOpen) => void;
}
const Shop: FC<IShop> = ({ isOpen, handleOpen, handleClose }) => {
const shopBlockRef = useRef<HTMLDivElement>(null);
const [isOverflow, setIsOverflow] = useState(false);
const { shopItems } = useShopItemsStore();
const { player } = usePlayerStore();
useEffect(() => {
const checkOverflow = () => {
if (shopBlockRef.current) {
setIsOverflow(
shopBlockRef.current.scrollHeight > shopBlockRef.current.clientHeight,
);
}
};
checkOverflow();
window.addEventListener('resize', checkOverflow);
return () => window.removeEventListener('resize', checkOverflow);
}, []);
return (
<div className={`${styles.shop} ${isOverflow ? styles.overflow : ''}`} ref={shopBlockRef}>
<div className={styles.content}>
<div className={styles.header}>
<span>Магазин</span>
</div>
<div className={styles.items}>
<div className={styles.itemsHeader}>
<span>Товары</span>
<span className={styles.itemsAll}>Все</span>
</div>
<div className={styles.shopItems}>
{shopItems &&
shopItems.map(shopItem => (
<ShopItem
key={shopItem.id}
shopItem={shopItem}
handleOpen={handleOpen}
/>
))}
</div>
</div>
<div className={styles.upgrades}>
<div className={styles.upgradesTitle}>
<span>Улучшения</span>
</div>
<div className={styles.upgradesContent}>
<span>Уже скоро...</span>
</div>
</div>
<ModalExchangeContainer
isOpen={isOpen}
balance={toFixed(player?.attributes.balance)}
onClose={handleClose}
/>
<ToastContainer />
</div>
</div>
);
};
export default Shop;

View File

@@ -0,0 +1,42 @@
'use client';
import Shop from '@/components/shop/Shop';
import { useCallback, useState } from 'react';
import { useModalStore } from '@/store/modal.state';
export interface IHandleOpen {
image?: string;
isDecimal?: boolean;
priceItem?: number;
shopItem?: number;
}
const ShopContainer = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const { setShopItemId, setPriceItem, setImage, setIsDecimal } = useModalStore();
const handleClose = useCallback(() => {
setIsOpen(false);
}, []);
const handleOpen = useCallback((data: IHandleOpen) => {
if (data.image && data.priceItem && data.shopItem && data.isDecimal !== undefined) {
setIsDecimal(data.isDecimal);
setImage(data.image);
setPriceItem(data.priceItem);
setShopItemId(data.shopItem);
setIsOpen(true);
} else {
setIsOpen(true);
setIsDecimal(true);
setImage('/icons/small-coin-icon.svg');
setPriceItem(0);
setShopItemId(null);
}
}, []);
return <Shop isOpen={isOpen} handleClose={handleClose} handleOpen={handleOpen} />;
};
export default ShopContainer;

View File

@@ -0,0 +1,63 @@
'use client';
import { FC } from 'react';
import { IShopItem } from '@/api/shop-item.api';
import styles from '@/components/shop/Shop.module.css';
import Image from 'next/image';
import { IHandleOpen } from '@/components/shop/ShopContainer';
interface IShopItemProps {
shopItem: IShopItem;
handleOpen: (data: IHandleOpen) => void;
}
const ShopItem: FC<IShopItemProps> = ({ shopItem, handleOpen }) => {
return (
<div
className={styles.item}
onClick={() =>
handleOpen({
image: `${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`,
isDecimal: shopItem.attributes.isDecimal,
priceItem: shopItem.attributes.price,
shopItem: shopItem.id,
})
}
>
{shopItem?.attributes?.icon?.data && (
<div className={styles.item_img}>
<img
loading={'lazy'}
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`}
alt={'shop item icon'}
/>
</div>
)}
<div className={styles.itemInfo}>
<div className={styles.itemInfoTitle}>
<span>{shopItem.attributes.name}</span>
</div>
<div className={styles.itemInfoContent}>
<span>{shopItem.attributes.price}</span>
<Image
src={'/icons/small-robucks-icon.svg'}
alt={'small robucks'}
width={18}
height={18}
/>
<span>=</span>
<span>1</span>
<div className={styles.item_img_sm}>
<img
loading={'lazy'}
src={`${process.env.NEXT_PUBLIC_STRAPI || ''}${shopItem.attributes.icon.data.attributes.url}`}
alt={'small coin icon'}
/>
</div>
</div>
</div>
</div>
);
};
export default ShopItem;

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;

View File

@@ -0,0 +1,84 @@
'use client';
import { FC } from 'react';
import styles from '@/components/home/Home.module.css';
import Image from 'next/image';
interface IButtonMining {
isLoading: boolean;
typeButton: 'pending' | 'farming' | 'claim_reward';
remainingTime: string | null;
farm_amount: number | undefined;
handleStartFarming: () => void;
handleClaimRewards: () => void;
}
const ButtonMining: FC<IButtonMining> = ({
isLoading,
typeButton,
remainingTime,
farm_amount,
handleStartFarming,
handleClaimRewards,
}) => {
if (typeButton === 'pending') {
return (
<>
<button
className={
isLoading
? `${styles.disabledButton} ${styles.miningButton}`
: `${styles.miningButton}`
}
disabled={isLoading}
onClick={handleStartFarming}
>
Начать добычу
</button>
</>
);
}
if (typeButton === 'farming' && remainingTime) {
return (
<>
<button
className={
isLoading
? `${styles.disabledButton} ${styles.farmingButton}`
: `${styles.farmingButton}`
}
>{`Собрать добычу через ${remainingTime}`}</button>
</>
);
}
if (typeButton === 'claim_reward') {
return (
<>
<button
className={
isLoading
? `${styles.disabledButton} ${styles.claimReward}`
: `${styles.claimReward}`
}
onClick={handleClaimRewards}
>
<div className={styles.reward}>
<span>Собрать добычу </span>
<div className={styles.claimAmount}>
<span>+</span>
<Image
src={'/icons/small-robucks-icon.svg'}
alt={'small robucks'}
width={18}
height={18}
/>
<span>{farm_amount}</span>
</div>
</div>
</button>
</>
);
}
};
export default ButtonMining;

View File

@@ -0,0 +1,56 @@
.container {
position: relative;
display: flex;
align-items: center;
}
.container button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
.toast {
position: fixed;
right: -500px;
bottom: 90px;
height: 89px;
border-radius: 20px;
padding: 12px 16px;
background: #000000;
border: 1px solid #143916;
transition: right 0.3s ease;
font-size: 16px;
font-weight: 600;
line-height: 21px;
font-family: var(--font-manrope);
}
.content {
display: flex;
align-items: flex-start;
gap: 7px;
}
.closeIcon {
display: flex;
justify-content: flex-end;
width: 100%;
height: auto;
cursor: pointer;
}
.info {
display: flex;
flex-direction: column;
}
.info span {
font-size: 14px;
font-weight: 400;
line-height: 19px;
}
.visible {
right: 20px;
}

View File

@@ -0,0 +1,42 @@
'use client';
import { FC } from 'react';
import styles from '@/components/ui/copy-link-toast/CopyLinkToast.module.css';
import Image from 'next/image';
interface ICopyLinkToast {
isVisible: boolean;
handleCloseToastLink: () => void;
}
const CopyLinkToast: FC<ICopyLinkToast> = ({ isVisible, handleCloseToastLink }) => {
return (
<div className={styles.container}>
<div className={`${styles.toast} ${isVisible ? styles.visible : ''}`}>
<div className={styles.closeIcon} onClick={handleCloseToastLink}>
<Image
src={'/icons/close-icon.svg'}
alt={'close icon'}
width={12}
height={12}
/>
</div>
<div className={styles.content}>
<div className={styles.img}>
<Image
src={'/icons/info-icon.svg'}
alt={'info icon'}
height={24}
width={24}
/>
</div>
<div className={styles.info}>
<p>Приглашение скопировано 🔗</p>
<span>Отправьте эту ссылку своим друзьям 📨</span>
</div>
</div>
</div>
</div>
);
};
export default CopyLinkToast;

View File

@@ -0,0 +1,238 @@
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
transition: opacity 0.5s ease;
opacity: 0;
pointer-events: none;
}
.modal.isOpen {
opacity: 1;
pointer-events: auto;
}
.modalContent {
border-radius: 16px 16px 0 0;
background: #1A1A1C;
padding: 24px 16px 0 16px;
width: 100%;
height: 85%;
position: fixed;
bottom: -85%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
transition: transform 0.5s ease;
}
.modalContent.isOpen {
transform: translateY(-100%);
}
.modalContent.isClosing {
transform: translateY(0);
}
.closeButton {
position: fixed;
top: 20px;
right: 20px;
width: 18px;
height: 18px;
cursor: pointer;
}
.closeButton::before,
.closeButton::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 18px;
height: 2px;
background: #FFFFFF26;
}
.closeButton::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.closeButton::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.exchangeRate {
margin: 10px 0;
}
.submitButton {
background-color: green;
color: white;
border: none;
padding: 10px;
cursor: pointer;
}
.modalTitle {
margin-top: 11px;
font-weight: 600;
font-size: 19px;
line-height: 20px;
color: white;
}
.content {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.5rem;
}
.selectAmount {
display: flex;
flex-direction: column;
width: 100%;
margin-top: 1.5rem;
}
.selectAmountTitle {
display: flex;
justify-content: flex-start;
padding-bottom: 0.5rem;
color: white;
}
.inputRange {
width: 100%;
position: absolute;
bottom: 0;
left: 0;
}
.inputRange input[type='range'] {
display: flex;
align-items: center;
width: 100%;
-webkit-appearance: none;
appearance: none;
height: 5px;
background: #333;
border-radius: 5px;
outline: none;
transition: opacity .2s;
}
.inputRange input[type='range']::-moz-range-progress {
background-color: #17da17;
}
.inputRange input[type='range']::-moz-range-track {
background: #333;
}
.inputRange input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: #42BB47;
border-radius: 50%;
cursor: pointer;
}
.inputRange input[type='range']::-moz-range-thumb {
width: 16px;
height: 16px;
background: #42BB47;
border-radius: 50%;
cursor: pointer;
}
.amount {
position: relative;
display: flex;
flex-direction: column;
border-radius: 16px 16px 0 0;
background: #FFFFFF0D;
padding: 16px;
color: white;
}
.rate {
background: #FFFFFF0D;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 56px;
border-radius: 1rem;
padding: 1rem;
gap: 5px;
}
.rate span {
font-size: 24px;
color: #BBBBBB;
line-height: 28px;
font-weight: 500;
}
.nickname {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.5rem;
margin-top: 1.5rem;
}
.nickname input {
background: #FFFFFF0D;
border-radius: 1rem;
padding: 1rem;
outline: none;
border: none;
height: 56px;
}
.nickname input::placeholder {
font-weight: 500;
color: #BBBBBB;
font-size: 14px;
line-height: 16px;
}
.nicknameTitle {
display: flex;
justify-content: flex-start;
}
.selectedAmount {
display: flex;
align-items: center;
gap: 5px;
justify-content: center;
}
.submitButton {
width: 100%;
background: #42BB47;
color: white;
border-radius: 30px;
padding: 1rem;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1.5rem;
font-weight: 600;
font-size: 16px;
line-height: 21px;
}

View File

@@ -0,0 +1,66 @@
'use client';
import { Dispatch, FC, RefObject, SetStateAction } from 'react';
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
import ModalExchangeContent from '@/components/ui/modal-exchange/ModalExchangeContent';
import { useModalStore } from '@/store/modal.state';
interface IModalExchange {
isOpen: boolean;
isClosing: boolean;
amount: number;
balance: number | undefined;
nickname: string;
rangeRef: RefObject<HTMLInputElement>;
setNickname: Dispatch<SetStateAction<string>>;
setAmount: Dispatch<SetStateAction<number>>;
onClose: () => void;
handleCreateExchange: (amount: number, total: number, nickname: string) => Promise<void>;
}
const ModalExchange: FC<IModalExchange> = ({
isOpen,
isClosing,
amount,
balance,
nickname,
rangeRef,
onClose,
setNickname,
setAmount,
handleCreateExchange,
}) => {
const { priceItem, image, isDecimal } = useModalStore();
return (
<div
className={`${styles.modal} ${isOpen ? styles.isOpen : ''} ${isClosing ? styles.isClosing : ''}`}
>
<div
className={`${styles.modalContent} ${isOpen ? styles.isOpen : ''} ${isClosing ? styles.isClosing : ''}`}
>
<div className={styles.closeButton} onClick={onClose} />
<div className={styles.modalTitle}>
<span>Обмен</span>
</div>
<ModalExchangeContent
isDecimal={isDecimal}
balance={balance}
nickname={nickname}
amount={amount}
image={image}
priceItem={priceItem}
rangeRef={rangeRef}
setNickname={setNickname}
setAmount={setAmount}
/>
<button
className={styles.submitButton}
onClick={() => handleCreateExchange(amount, amount / +priceItem, nickname)}
>
Вывод
</button>
</div>
</div>
);
};
export default ModalExchange;

View File

@@ -0,0 +1,116 @@
'use client';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import ModalExchange from '@/components/ui/modal-exchange/ModalExchange';
import { usePlayerStore } from '@/store/player.state';
import { useConfigStore } from '@/store/config.state';
import { useNotify } from '@/hooks/useNotify';
import { ExchangeRequestApi } from '@/api/exchange-request.api';
import { useModalStore } from '@/store/modal.state';
import { useTokenStore } from '@/store/token.state';
import { toFixed } from '@/utils/FormatNumber';
interface IModalExchangeContainer {
isOpen: boolean;
balance: number | undefined;
onClose: () => void;
}
const ModalExchangeContainer: FC<IModalExchangeContainer> = ({ balance, isOpen, onClose }) => {
const { token } = useTokenStore();
const [amount, setAmount] = useState<number>(1);
const [nickname, setNickname] = useState<string>('');
const [isClosing, setIsClosing] = useState<boolean>(false);
const rangeRef = useRef<HTMLInputElement>(null);
const { player, setPlayer } = usePlayerStore();
const { config } = useConfigStore();
const { shopItemId, isDecimal, image, setShopItemId, setIsDecimal } = useModalStore();
const { getSuccessNotify, getErrorNotifyByCount, getErrorNotifyByNickname } = useNotify();
useEffect(() => {
if (rangeRef.current) {
rangeRef.current.style.setProperty('--value', amount.toString());
}
}, [amount]);
useEffect(() => {
if (isOpen) {
setIsClosing(false);
setAmount(0);
}
}, [isOpen]);
const handleClose = () => {
setIsClosing(true);
setTimeout(() => {
onClose();
}, 500);
};
const handleCreateExchange = useCallback(
async (amount: number, total: number, nickname: string) => {
if (!token) {
return;
}
const exchangeRequestApi = ExchangeRequestApi.getInstance(token);
if (player && config) {
if (nickname.length === 0) {
getErrorNotifyByNickname();
} else if (total === 0) {
getErrorNotifyByCount();
} else {
exchangeRequestApi
.createExchange({
amount: amount,
total: total,
playerId: player.id,
nickname: nickname,
balance: player.attributes.balance,
telegram_id: config.attributes.admins.data.map(
user => user.attributes.telegram_id,
),
shop_item: shopItemId || undefined,
archive_request_id: player!.attributes.archive_request!.data?.id || -1,
})
.then(() => {
if (player) {
setPlayer({
...player,
attributes: {
...player.attributes,
balance: toFixed(player?.attributes.balance) - amount,
},
});
setShopItemId(null);
setIsDecimal(true);
}
});
getSuccessNotify();
handleClose();
}
}
},
[player, token, shopItemId],
);
return (
<ModalExchange
isOpen={isOpen}
isClosing={isClosing}
balance={balance}
amount={amount}
nickname={nickname}
rangeRef={rangeRef}
onClose={handleClose}
setNickname={setNickname}
setAmount={setAmount}
handleCreateExchange={handleCreateExchange}
/>
);
};
export default ModalExchangeContainer;

View File

@@ -0,0 +1,76 @@
'use client';
import { Dispatch, FC, RefObject, SetStateAction } from 'react';
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
import Image from 'next/image';
import NicknameInput from '@/components/ui/modal-exchange/inputs/NicknameInput';
import RangeInput from '@/components/ui/modal-exchange/inputs/RangeInput';
import { toFixed } from '@/utils/FormatNumber';
interface IModalExchangeContent {
isDecimal: boolean;
balance: number | undefined;
nickname: string;
amount: number;
priceItem: number;
image: string;
rangeRef: RefObject<HTMLInputElement>;
setNickname: Dispatch<SetStateAction<string>>;
setAmount: Dispatch<SetStateAction<number>>;
}
const ModalExchangeContent: FC<IModalExchangeContent> = ({
isDecimal,
balance,
nickname,
amount,
priceItem,
image,
rangeRef,
setNickname,
setAmount,
}) => {
return (
<div className={styles.content}>
<div className={styles.selectAmount}>
<span className={styles.selectAmountTitle}>Выберите количество</span>
<div className={styles.amount}>
<div className={styles.selectedAmount}>
<span>{`${amount.toFixed(isDecimal ? 3 : 0)} - ${toFixed(balance)}`}</span>
<Image
src={'/icons/small-robucks-icon.svg'}
alt={'small robucks icon'}
height={18}
width={18}
/>
</div>
<RangeInput
isDecimal={isDecimal}
balance={balance}
amount={amount}
priceItem={priceItem}
rangeRef={rangeRef}
setAmount={setAmount}
/>
</div>
</div>
<div>
<Image
src={'/icons/exchange-icon.svg'}
alt={'exchange icon'}
width={24}
height={24}
/>
</div>
<div className={styles.rate}>
{priceItem && (
<span>{isDecimal ? (amount / priceItem).toFixed(3) : amount / priceItem}</span>
)}
<Image src={image} alt={'small coin icon'} height={24} width={24} />
</div>
<NicknameInput nickname={nickname} setNickname={setNickname} />
</div>
);
};
export default ModalExchangeContent;

View File

@@ -0,0 +1,28 @@
'use client';
import React, { Dispatch, FC, SetStateAction, useState } from 'react';
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
interface INicknameInput {
nickname: string;
setNickname: Dispatch<SetStateAction<string>>;
}
const NicknameInput: FC<INicknameInput> = ({ nickname, setNickname }) => {
const handleChangeNickname = (e: React.ChangeEvent<HTMLInputElement>) => {
setNickname(e.target.value);
};
return (
<div className={styles.nickname}>
<span className={styles.nicknameTitle}>Укажите ваш профиль в Roblox</span>
<input
placeholder={'Ник в Roblox'}
type={'text'}
value={nickname}
onChange={handleChangeNickname}
/>
</div>
);
};
export default NicknameInput;

View File

@@ -0,0 +1,48 @@
'use client';
import { Dispatch, FC, SetStateAction } from 'react';
import styles from '@/components/ui/modal-exchange/ModalExchange.module.css';
interface IRangeInput {
isDecimal: boolean;
balance: number | undefined;
amount: number;
priceItem: number;
rangeRef: React.RefObject<HTMLInputElement>;
setAmount: Dispatch<SetStateAction<number>>;
}
const RangeInput: FC<IRangeInput> = ({
isDecimal,
balance,
priceItem,
amount,
rangeRef,
setAmount,
}) => {
const handleRangeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = parseFloat(e.target.value);
if (!isDecimal) {
value = Math.round(value / priceItem) * priceItem;
} else {
value = parseFloat(value.toFixed(6));
}
setAmount(value);
};
return (
<div className={styles.inputRange}>
<input
type='range'
id='amountRange'
min='0'
max={balance}
step={isDecimal ? '0.000001' : priceItem.toString()}
value={amount}
onChange={handleRangeChange}
ref={rangeRef}
/>
</div>
);
};
export default RangeInput;

View File

@@ -0,0 +1,40 @@
.container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
padding-bottom: 1rem;
padding-top: 1rem;
position: fixed;
height: 82px;
width: 100%;
background: #101010;
bottom: 0;
z-index: 1;
}
.nav_menu, .active_nav_menu {
display: flex;
flex-direction: column;
align-items: center;
font-size: 12px;
font-weight: 600;
line-height: 16.3px;
gap: 3px;
}
.countTasks {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
right: 10%;
top: -10%;
background: #D92000;
}
.active_nav_menu span, .active_nav_menu img {
color: #42BB47;
}

View File

@@ -0,0 +1,126 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import styles from './Navigation.module.css';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { usePlayerStore } from '@/store/player.state';
import { useTasksStore } from '@/store/tasks.state';
import { ProvidersProps } from '@/components/providers/Providers';
const Navigation = ({ id }: Pick<ProvidersProps, 'id'>) => {
const pathname = usePathname();
const { player } = usePlayerStore();
const [isLoading, setIsLoading] = useState<boolean>(true);
const { tasks } = useTasksStore();
const countTasks = tasks.length - (player?.attributes.finished_tasks.data?.length || 0);
useEffect(() => {
if (isLoading) {
setIsLoading(false);
}
}, [isLoading]);
const getNavLink = (endpoint: string) => {
return `/${id}/${endpoint}`;
};
return (
<div className={styles.container}>
<Link href={getNavLink('home')} style={{ textDecoration: 'none' }}>
<div className={pathname === '/' ? styles.active_nav_menu : styles.nav_menu}>
{pathname === '/' ? (
<Image
src={'/icons/pickaxe-icon-selected.svg'}
alt={'pickaxe-icon'}
width={24}
height={24}
/>
) : (
<Image
src={'/icons/pickaxe-icon.svg'}
alt={'pickaxe-icon'}
width={24}
height={24}
/>
)}
<span>Главная</span>
</div>
</Link>
<Link href={getNavLink('shop')} style={{ textDecoration: 'none' }}>
<div className={pathname === '/shop' ? styles.active_nav_menu : styles.nav_menu}>
{pathname === '/shop' ? (
<Image
src={'/icons/shop-icon-selected.svg'}
alt={'shop-icon'}
width={24}
height={24}
/>
) : (
<Image
src={'/icons/shop-icon.svg'}
alt={'shop-icon'}
width={24}
height={24}
/>
)}
<span>Магазин</span>
</div>
</Link>
<Link
href={getNavLink('tasks')}
style={{ textDecoration: 'none', position: 'relative', height: '82px' }}
>
{countTasks !== 0 && <div className={styles.countTasks}>{countTasks}</div>}
<div
className={
pathname === '/tasks'
? `${styles.active_nav_menu} ${styles.task}`
: `${styles.nav_menu} ${styles.task}`
}
>
{pathname === '/tasks' ? (
<Image
src={'/icons/task-list-icon-selected.svg'}
alt={'task-list-icon'}
width={24}
height={24}
/>
) : (
<Image
src={'/icons/task-list-icon.svg'}
alt={'task-list-icon'}
width={24}
height={24}
/>
)}
<span>Задания</span>
</div>
</Link>
<Link href={getNavLink('friends')} style={{ textDecoration: 'none' }}>
<div className={pathname === '/friends' ? styles.active_nav_menu : styles.nav_menu}>
{pathname === '/friends' ? (
<Image
src={'/icons/friends-icon-selected.svg'}
alt={'friends-icon'}
width={24}
height={24}
/>
) : (
<Image
src={'/icons/friends-icon.svg'}
alt={'friends-icon'}
width={24}
height={24}
/>
)}
<span>Друзья</span>
</div>
</Link>
</div>
);
};
export default Navigation;

View File

@@ -0,0 +1,6 @@
.wrapper {
max-width: 100%;
min-height: 100vh;
position: relative;
overflow-y: hidden;
}

View File

@@ -0,0 +1,12 @@
import {PropsWithChildren} from "react";
import styles from './Wrapper.module.css'
const Wrapper = ({ children } : PropsWithChildren) => {
return(
<div className={styles.wrapper}>
{children}
</div>
);
};
export default Wrapper;

View File

@@ -0,0 +1,10 @@
module.exports = {
apps: [
{
name: 'webapp',
script: 'npm run build && npm run start',
time: true,
log_date_format: 'DD.MM.YYYY HH:mm:ss',
},
],
};

View File

@@ -0,0 +1,53 @@
import { toast } from 'react-toastify';
interface IUseNotifyReturn {
getSuccessNotify: () => number | string;
getErrorNotifyByNickname: () => number | string;
getErrorNotifyByCount: () => number | string;
}
export function useNotify(): IUseNotifyReturn {
const getSuccessNotify = () => {
return toast.success('Заявка успешно создана!', {
position: 'top-left',
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
style: { width: '250px', color: 'black', margin: '15px' },
progress: undefined,
theme: 'dark',
});
};
const getErrorNotifyByNickname = () => {
return toast.error('Вы не указали ваш никнейм!', {
position: 'top-left',
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
style: { width: '250px', color: 'black', margin: '15px' },
progress: undefined,
theme: 'dark',
});
};
const getErrorNotifyByCount = () => {
return toast.error('Вы не можете выбрать количество равное 0!', {
position: 'top-left',
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
style: { width: '250px', color: 'black', margin: '15px' },
progress: undefined,
theme: 'dark',
});
};
return { getSuccessNotify, getErrorNotifyByNickname, getErrorNotifyByCount };
}

View File

@@ -0,0 +1,7 @@
export {};
declare global {
interface Window {
Telegram: any;
}
}

View File

@@ -0,0 +1,40 @@
import { StatusTask } from '@/interfaces/task.type';
import { ITask } from '@/api/tasks.api';
import { IArchiveRequest } from '@/api/archive-request.api';
export interface IPlayer {
id: number;
attributes: {
telegram_nick: string;
telegram_id: number;
auth_id: string;
balance: number;
old_balance: number;
referral_fee: number;
my_referrals: { data: IPlayer[] | null };
referral: { data: IPlayer | null };
start_farm: Date | null;
finished_tasks: { data: IFinishedTasks[] | null };
archive_request: { data: IArchiveRequest | null };
};
}
export interface IFinishedTasks {
id: number;
attributes: {
status: StatusTask;
task: { data: ITask | null };
};
}
export interface ICreatePlayer {
telegram_id: number;
telegram_nick: string;
balance: number;
old_balance: number;
}
export interface IUpdatePlayerTask {
status: StatusTask;
taskId: number;
}

View File

@@ -0,0 +1,4 @@
export enum StatusTask {
'pending_prize' = 'pending_prize',
'claimed' = 'claimed',
}

View File

@@ -0,0 +1,28 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
hostname: 'strapi.rbx-click.ru'
},
{
protocol: 'http',
hostname: '127.0.0.1'
},
{
protocol: 'http',
hostname: 'localhost'
}
]
},
async rewrites() {
return [
{
source: '/uploads/:path*',
destination: 'http://127.0.0.1:1337/uploads/:path*',
},
];
},
};
export default nextConfig;

5300
CMyTapper/robucks-front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
{
"name": "clicker-front",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@next/font": "^14.2.4",
"@types/axios": "^0.14.0",
"@types/js-cookie": "^3.0.6",
"@types/react-toastify": "^4.1.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"axios": "^1.7.2",
"bcryptjs": "^2.4.3",
"crypto-js": "^4.2.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^4.0.0",
"js-cookie": "^3.0.5",
"next": "14.2.4",
"prettier": "^3.3.3",
"react": "^18",
"react-dom": "^18",
"react-toastify": "^10.0.5",
"zustand": "^4.5.4"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/crypto-js": "^4.2.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "8",
"eslint-config-next": "14.2.4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,15 @@
:root {
--font-manrope: var(--font-manrope);
--font-clash: var(--font-clash);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
color: white;
}
body {
min-height: 100vh;
}

View File

@@ -0,0 +1,11 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_81_2696)">
<path d="M1 1L11 11" stroke="white" stroke-linecap="round"/>
<path d="M1 11L11 1" stroke="white" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_81_2696">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -0,0 +1,6 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 8L7.5 5L4.5 8" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 19V5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.5 16L17.5 19L20.5 16" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.5 5V19" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@@ -0,0 +1,5 @@
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32.1369 9.86319C38.2877 16.014 38.2877 25.9863 32.1369 32.137C25.9861 38.2878 16.0138 38.2878 9.86307 32.137C3.71231 25.9863 3.71231 16.0139 9.86307 9.86319C16.0138 3.71243 25.9862 3.71243 32.1369 9.86319" stroke="white" stroke-width="2.625" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M24.4806 14.567C26.403 16.4894 26.403 19.6063 24.4806 21.5288C22.5581 23.4512 19.4412 23.4512 17.5187 21.5288C15.5963 19.6063 15.5963 16.4894 17.5187 14.567C19.4412 12.6445 22.5581 12.6445 24.4806 14.567" stroke="white" stroke-width="2.625" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M30.9872 33.1766C28.4759 30.5324 24.9339 28.8751 20.9999 28.8751C17.0659 28.8751 13.5239 30.5324 11.0127 33.1784" stroke="white" stroke-width="2.625" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@@ -0,0 +1,8 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 17C8.88071 17 10 15.8807 10 14.5C10 13.1193 8.88071 12 7.5 12C6.11929 12 5 13.1193 5 14.5C5 15.8807 6.11929 17 7.5 17Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M17.5 17C18.8807 17 20 15.8807 20 14.5C20 13.1193 18.8807 12 17.5 12C16.1193 12 15 13.1193 15 14.5C15 15.8807 16.1193 17 17.5 17Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12.5 7C13.8807 7 15 5.88071 15 4.5C15 3.11929 13.8807 2 12.5 2C11.1193 2 10 3.11929 10 4.5C10 5.88071 11.1193 7 12.5 7Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12.5 22C12.5 19.2386 10.2614 17 7.5 17C4.73857 17 2.5 19.2386 2.5 22" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22.5 22C22.5 19.2386 20.2614 17 17.5 17C14.7386 17 12.5 19.2386 12.5 22" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M17.5 12C17.5 9.2386 15.2614 7 12.5 7C9.7386 7 7.5 9.2386 7.5 12" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,8 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.125 17.8956C8.50571 17.8956 9.625 16.7763 9.625 15.3956C9.625 14.0149 8.50571 12.8956 7.125 12.8956C5.74429 12.8956 4.625 14.0149 4.625 15.3956C4.625 16.7763 5.74429 17.8956 7.125 17.8956Z" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M17.125 17.8956C18.5057 17.8956 19.625 16.7763 19.625 15.3956C19.625 14.0149 18.5057 12.8956 17.125 12.8956C15.7443 12.8956 14.625 14.0149 14.625 15.3956C14.625 16.7763 15.7443 17.8956 17.125 17.8956Z" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12.125 7.89563C13.5057 7.89563 14.625 6.77634 14.625 5.39563C14.625 4.01492 13.5057 2.89563 12.125 2.89563C10.7443 2.89563 9.625 4.01492 9.625 5.39563C9.625 6.77634 10.7443 7.89563 12.125 7.89563Z" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12.125 22.8956C12.125 20.1342 9.8864 17.8956 7.125 17.8956C4.36357 17.8956 2.125 20.1342 2.125 22.8956" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22.125 22.8956C22.125 20.1342 19.8864 17.8956 17.125 17.8956C14.3636 17.8956 12.125 20.1342 12.125 22.8956" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M17.125 12.8956C17.125 10.1342 14.8864 7.89563 12.125 7.89563C9.3636 7.89563 7.125 10.1342 7.125 12.8956" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C14.7614 22 17.2614 20.8807 19.0711 19.0711C20.8807 17.2614 22 14.7614 22 12C22 9.2386 20.8807 6.7386 19.0711 4.92893C17.2614 3.11929 14.7614 2 12 2C9.2386 2 6.7386 3.11929 4.92893 4.92893C3.11929 6.7386 2 9.2386 2 12C2 14.7614 3.11929 17.2614 4.92893 19.0711C6.7386 20.8807 9.2386 22 12 22Z" fill="white" stroke="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5.5C12.6904 5.5 13.25 6.05965 13.25 6.75C13.25 7.44035 12.6904 8 12 8C11.3097 8 10.75 7.44035 10.75 6.75C10.75 6.05965 11.3097 5.5 12 5.5Z" fill="black"/>
<path d="M12.25 17V10H11.75H11.25" stroke="black" stroke-linecap="round"/>
<path d="M10.5 17H14" stroke="black" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 786 B

View File

@@ -0,0 +1,10 @@
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.5333 7.04858C29.5867 7.65627 30.4615 8.53017 31.07 9.58253C31.6785 10.6349 31.9992 11.8287 32 13.0441V23.5429C32.0006 24.7586 31.6804 25.9529 31.0718 27.0055C30.4632 28.0581 29.5876 28.9318 28.5333 29.5384L19.4667 34.7745C18.4131 35.3839 17.2173 35.7048 16 35.7048C14.7827 35.7048 13.5869 35.3839 12.5333 34.7745L3.46667 29.5118C2.41632 28.9074 1.54326 28.0379 0.934872 26.9904C0.326483 25.9429 0.00410371 24.7541 0 23.5429L0 13.0441C0.000779536 11.8287 0.321513 10.6349 0.930001 9.58253C1.53849 8.53017 2.41332 7.65627 3.46667 7.04858L12.5333 1.82582C13.5869 1.21644 14.7827 0.895554 16 0.895554C17.2173 0.895554 18.4131 1.21644 19.4667 1.82582L28.5333 7.04858ZM13.7467 4.19738L4.92 9.30023C4.23356 9.69153 3.66331 10.2578 3.26744 10.9413C2.87157 11.6248 2.66426 12.401 2.66667 13.1906V23.383C2.66563 24.1724 2.87352 24.9481 3.26925 25.6313C3.66498 26.3146 4.23448 26.8811 4.92 27.2735L13.7467 32.3763C14.4326 32.769 15.2094 32.9755 16 32.9755C16.7906 32.9755 17.5674 32.769 18.2533 32.3763L27.08 27.2735C27.7655 26.8811 28.335 26.3146 28.7308 25.6313C29.1265 24.9481 29.3344 24.1724 29.3333 23.383V13.1906C29.3357 12.401 29.1284 11.6248 28.7326 10.9413C28.3367 10.2578 27.7664 9.69153 27.08 9.30023L18.2533 4.19738C17.5674 3.80472 16.7906 3.59814 16 3.59814C15.2094 3.59814 14.4326 3.80472 13.7467 4.19738ZM17.8533 7.04858L24.8133 11.0456C25.3715 11.3675 25.8359 11.8293 26.1609 12.3854C26.486 12.9415 26.6603 13.5726 26.6667 14.2165V22.2106C26.6676 22.8596 26.4967 23.4973 26.1711 24.0589C25.8456 24.6205 25.3772 25.086 24.8133 25.4082L17.8533 29.4052C17.2907 29.7329 16.6512 29.9056 16 29.9056C15.3488 29.9056 14.7093 29.7329 14.1467 29.4052L7.18667 25.4082C6.63638 25.094 6.17662 24.6431 5.85192 24.0993C5.52722 23.5554 5.34859 22.937 5.33333 22.3038V14.2165C5.34204 13.5749 5.51744 12.9465 5.84233 12.393C6.16723 11.8394 6.63047 11.3797 7.18667 11.0589L14.1467 7.0619C14.7093 6.73418 15.3488 6.56151 16 6.56151C16.6512 6.56151 17.2907 6.73418 17.8533 7.0619V7.04858ZM12 22.2772H20V14.2832H12V22.2772Z" fill="url(#paint0_linear_81_2197)"/>
<defs>
<linearGradient id="paint0_linear_81_2197" x1="15.84" y1="2.43203" x2="15.84" y2="36.1628" gradientUnits="userSpaceOnUse">
<stop stop-color="#EECF56"/>
<stop offset="0.59" stop-color="#B79F41"/>
<stop offset="1" stop-color="#ECCF5F"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.6331 7.33314L1.76367 21.1865C0.929578 21.9992 2.23953 23.8257 3.39443 22.7906L17.3013 8.92113M15.6331 7.33314L17.3013 8.92113M15.6331 7.33314C15.4335 7.09967 15.2171 6.45628 15.9486 5.75051M17.3013 8.92113C17.8488 9.30609 18.2815 9.08153 18.4295 8.92113C18.5154 8.84252 18.6231 8.743 18.7449 8.62936M15.9486 5.75051C16.4579 5.25904 17.0331 4.72868 17.451 4.349M15.9486 5.75051C13.7115 3.56476 9.51288 1.95611 7.69321 1.425C11.5343 -0.063536 15.7989 2.75411 17.451 4.349M20.4184 6.98026C20.9531 6.39426 20.766 5.78794 20.6056 5.55803C20.3899 5.3495 19.825 4.78596 19.2903 4.19995C18.7556 3.61395 18.1336 3.79182 17.8894 3.954C17.7855 4.04654 17.6332 4.18345 17.451 4.349M20.4184 6.98026C19.9881 7.4519 19.2472 8.16069 18.7449 8.62936M20.4184 6.98026C23.3613 10.291 24.1986 14.979 22.9207 16.6953C22.386 13.7738 19.9141 10.1007 18.7449 8.62936" stroke="#42BB47" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 997 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0081 7.22876L1.13867 21.0822C0.304578 21.8949 1.61453 23.7213 2.76943 22.6862L16.6763 8.81674M15.0081 7.22876L16.6763 8.81674M15.0081 7.22876C14.8085 6.99528 14.5921 6.35189 15.3236 5.64612M16.6763 8.81674C17.2238 9.20171 17.6565 8.97714 17.8045 8.81674C17.8904 8.73814 17.9981 8.63862 18.1199 8.52498M15.3236 5.64612C15.8329 5.15465 16.4081 4.62429 16.826 4.24462M15.3236 5.64612C13.0865 3.46038 8.88788 1.85172 7.06821 1.32061C10.9093 -0.167921 15.1739 2.64973 16.826 4.24462M19.7934 6.87587C20.3281 6.28987 20.141 5.68355 19.9806 5.45364C19.7649 5.24512 19.2 4.68157 18.6653 4.09557C18.1306 3.50957 17.5086 3.68743 17.2644 3.84962C17.1605 3.94216 17.0082 4.07906 16.826 4.24462M19.7934 6.87587C19.3631 7.34752 18.6222 8.0563 18.1199 8.52498M19.7934 6.87587C22.7363 10.1866 23.5736 14.8746 22.2957 16.5909C21.761 13.6694 19.2891 9.99634 18.1199 8.52498" stroke="#BBBBBB" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

View File

@@ -0,0 +1,8 @@
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.08333 10.4168C4.88875 10.4168 5.54167 9.76387 5.54167 8.95846C5.54167 8.15304 4.88875 7.50012 4.08333 7.50012C3.27792 7.50012 2.625 8.15304 2.625 8.95846C2.625 9.76387 3.27792 10.4168 4.08333 10.4168Z" stroke="#BBBBBB" stroke-width="0.875" stroke-linecap="round"/>
<path d="M9.91683 10.4168C10.7222 10.4168 11.3752 9.76387 11.3752 8.95846C11.3752 8.15304 10.7222 7.50012 9.91683 7.50012C9.11141 7.50012 8.4585 8.15304 8.4585 8.95846C8.4585 9.76387 9.11141 10.4168 9.91683 10.4168Z" stroke="#BBBBBB" stroke-width="0.875" stroke-linecap="round"/>
<path d="M7.00008 4.58341C7.8055 4.58341 8.45841 3.9305 8.45841 3.12508C8.45841 2.31967 7.8055 1.66675 7.00008 1.66675C6.19467 1.66675 5.54175 2.31967 5.54175 3.12508C5.54175 3.9305 6.19467 4.58341 7.00008 4.58341Z" stroke="#BBBBBB" stroke-width="0.875" stroke-linecap="round"/>
<path d="M7.00008 13.3334C7.00008 11.7226 5.69423 10.4167 4.08341 10.4167C2.47258 10.4167 1.16675 11.7226 1.16675 13.3334" stroke="#BBBBBB" stroke-width="0.875" stroke-linecap="round"/>
<path d="M12.8333 13.3334C12.8333 11.7226 11.5275 10.4167 9.91667 10.4167C8.30585 10.4167 7 11.7226 7 13.3334" stroke="#BBBBBB" stroke-width="0.875" stroke-linecap="round"/>
<path d="M9.91683 7.50004C9.91683 5.88922 8.61098 4.58337 7.00016 4.58337C5.38935 4.58337 4.0835 5.88922 4.0835 7.50004" stroke="#BBBBBB" stroke-width="0.875" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,11 @@
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.2003 21.8812L29.2003 21.8853C29.201 22.3521 29.201 25.2462 29.2008 27.8971C29.2007 28.9121 28.3058 29.7352 27.201 29.7352H5.19975C4.09705 29.7352 3.20216 28.9137 3.20198 27.8966L3.20094 22.1048C3.20218 22.0725 3.2028 22.0401 3.2028 22.0077L3.20237 10.8737C3.20921 10.338 3.20904 7.59547 3.20747 5.09436C3.20682 4.07657 4.10355 3.25205 5.20904 3.25202L27.2003 3.25197C28.3049 3.252 29.2003 4.07532 29.2003 5.09094L29.2003 21.8812ZM0.202797 22.0078L0.202365 10.8564C0.209113 10.3992 0.209078 7.6484 0.207467 5.09596C0.205864 2.55475 2.44527 0.49361 5.209 0.493576L27.2003 0.49353C29.9617 0.493552 32.2003 2.55187 32.2003 5.09094V21.8812C32.201 22.3508 32.201 25.2484 32.2008 27.8973C32.2006 30.4361 29.9621 32.4937 27.201 32.4937H5.19975C2.43865 32.4937 0.20243 30.4358 0.201977 27.897L0.200928 22.0119L0.201862 22.0099L0.202797 22.0078ZM24.2006 6.17166L16.201 6.17159L8.20485 6.17169C7.09928 6.1717 6.20343 7.06769 6.20437 8.17326C6.20586 9.94752 6.20684 12.3091 6.20183 12.6485L6.2021 19.6188C6.2021 19.6191 6.20183 19.6194 6.20151 19.6194C6.20119 19.6194 6.20093 19.6196 6.20093 19.6199L6.20174 24.1721C6.20194 25.2765 7.09439 26.1717 8.19882 26.1717H16.2012H24.2008C25.3053 26.1717 26.2007 25.2772 26.2008 24.1727C26.201 22.3487 26.2011 19.8856 26.2005 19.539V8.17166C26.2005 7.06709 25.3051 6.17167 24.2006 6.17166ZM14.4514 19.5964C13.3469 19.5964 12.4514 18.701 12.4514 17.5964V14.7059C12.4514 13.6013 13.3469 12.7059 14.4514 12.7059H17.9509C19.0555 12.7059 19.9509 13.6013 19.9509 14.7059V17.5964C19.9509 18.701 19.0555 19.5964 17.9509 19.5964H14.4514Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.2003 21.8812L29.2003 21.8853C29.201 22.3521 29.201 25.2462 29.2008 27.8971C29.2007 28.9121 28.3058 29.7352 27.201 29.7352H5.19975C4.09705 29.7352 3.20216 28.9137 3.20198 27.8966L3.20094 22.1048C3.20218 22.0725 3.2028 22.0401 3.2028 22.0077L3.20237 10.8737C3.20921 10.338 3.20904 7.59547 3.20747 5.09436C3.20682 4.07657 4.10355 3.25205 5.20904 3.25202L27.2003 3.25197C28.3049 3.252 29.2003 4.07532 29.2003 5.09094L29.2003 21.8812ZM0.202797 22.0078L0.202365 10.8564C0.209113 10.3992 0.209078 7.6484 0.207467 5.09596C0.205864 2.55475 2.44527 0.49361 5.209 0.493576L27.2003 0.49353C29.9617 0.493552 32.2003 2.55187 32.2003 5.09094V21.8812C32.201 22.3508 32.201 25.2484 32.2008 27.8973C32.2006 30.4361 29.9621 32.4937 27.201 32.4937H5.19975C2.43865 32.4937 0.20243 30.4358 0.201977 27.897L0.200928 22.0119L0.201862 22.0099L0.202797 22.0078ZM24.2006 6.17166L16.201 6.17159L8.20485 6.17169C7.09928 6.1717 6.20343 7.06769 6.20437 8.17326C6.20586 9.94752 6.20684 12.3091 6.20183 12.6485L6.2021 19.6188C6.2021 19.6191 6.20183 19.6194 6.20151 19.6194C6.20119 19.6194 6.20093 19.6196 6.20093 19.6199L6.20174 24.1721C6.20194 25.2765 7.09439 26.1717 8.19882 26.1717H16.2012H24.2008C25.3053 26.1717 26.2007 25.2772 26.2008 24.1727C26.201 22.3487 26.2011 19.8856 26.2005 19.539V8.17166C26.2005 7.06709 25.3051 6.17167 24.2006 6.17166ZM14.4514 19.5964C13.3469 19.5964 12.4514 18.701 12.4514 17.5964V14.7059C12.4514 13.6013 13.3469 12.7059 14.4514 12.7059H17.9509C19.0555 12.7059 19.9509 13.6013 19.9509 14.7059V17.5964C19.9509 18.701 19.0555 19.5964 17.9509 19.5964H14.4514Z" fill="url(#paint0_linear_81_2041)"/>
<defs>
<linearGradient id="paint0_linear_81_2041" x1="16.0409" y1="1.90601" x2="16.0409" y2="32.9146" gradientUnits="userSpaceOnUse">
<stop stop-color="#42BB47"/>
<stop offset="0.59" stop-color="#33A137"/>
<stop offset="1" stop-color="#42BB47"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,6 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 6H22.5V10L21.8008 10.4195C20.6924 11.0846 19.3076 11.0846 18.1992 10.4195L17.5 10L16.8008 10.4195C15.6924 11.0846 14.3077 11.0846 13.1993 10.4195L12.5 10L11.8007 10.4195C10.6923 11.0846 9.30765 11.0846 8.19925 10.4195L7.5 10L6.80075 10.4195C5.69235 11.0846 4.30765 11.0846 3.19927 10.4195L2.5 10V6Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M4.5 11.2444V22H20.5V11" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M4.5 5.9111V2H20.5V6" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M15 16H10V22H15V16Z" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 756 B

View File

@@ -0,0 +1,6 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.625 6.89563H22.625V10.8956L21.9258 11.3152C20.8174 11.9802 19.4326 11.9802 18.3242 11.3152L17.625 10.8956L16.9258 11.3152C15.8174 11.9802 14.4327 11.9802 13.3243 11.3152L12.625 10.8956L11.9257 11.3152C10.8173 11.9802 9.43265 11.9802 8.32425 11.3152L7.625 10.8956L6.92575 11.3152C5.81735 11.9802 4.43265 11.9802 3.32427 11.3152L2.625 10.8956V6.89563Z" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M4.625 12.1401V22.8956H20.625V11.8956" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M4.625 6.80673V2.89563H20.625V6.89563" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M15.125 16.8956H10.125V22.8956H15.125V16.8956Z" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 863 B

View File

@@ -0,0 +1,15 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_81_2209)">
<path d="M16.05 3.46239C16.6425 3.80422 17.1346 4.29578 17.4769 4.88774C17.8191 5.47969 17.9996 6.15121 18 6.83487V12.7404C18.0003 13.4243 17.8202 14.0961 17.4779 14.6882C17.1355 15.2802 16.643 15.7717 16.05 16.1129L10.95 19.0582C10.3574 19.401 9.68475 19.5815 9 19.5815C8.31525 19.5815 7.64263 19.401 7.05 19.0582L1.95 16.0979C1.35918 15.758 0.868085 15.2689 0.525866 14.6797C0.183647 14.0904 0.00230833 13.4217 0 12.7404L0 6.83487C0.000438489 6.15121 0.180851 5.47969 0.523125 4.88774C0.8654 4.29578 1.35749 3.80422 1.95 3.46239L7.05 0.524585C7.64263 0.181809 8.31525 0.00131226 9 0.00131226C9.68475 0.00131226 10.3574 0.181809 10.95 0.524585L16.05 3.46239ZM7.7325 1.85859L2.7675 4.72894C2.38138 4.94905 2.06061 5.26759 1.83793 5.65204C1.61526 6.0365 1.49865 6.4731 1.5 6.9173V12.6505C1.49942 13.0946 1.61635 13.5309 1.83895 13.9152C2.06155 14.2995 2.3819 14.6182 2.7675 14.8389L7.7325 17.7092C8.11836 17.9301 8.55532 18.0463 9 18.0463C9.44468 18.0463 9.88164 17.9301 10.2675 17.7092L15.2325 14.8389C15.6181 14.6182 15.9384 14.2995 16.161 13.9152C16.3836 13.5309 16.5006 13.0946 16.5 12.6505V6.9173C16.5014 6.4731 16.3847 6.0365 16.1621 5.65204C15.9394 5.26759 15.6186 4.94905 15.2325 4.72894L10.2675 1.85859C9.88164 1.63772 9.44468 1.52152 9 1.52152C8.55532 1.52152 8.11836 1.63772 7.7325 1.85859ZM10.0425 3.46239L13.9575 5.71071C14.2714 5.8918 14.5327 6.15156 14.7155 6.46435C14.8983 6.77714 14.9964 7.13216 15 7.49437V11.991C15.0005 12.3561 14.9044 12.7148 14.7213 13.0307C14.5382 13.3466 14.2747 13.6085 13.9575 13.7897L10.0425 16.038C9.72604 16.2223 9.3663 16.3195 9 16.3195C8.6337 16.3195 8.27396 16.2223 7.9575 16.038L4.0425 13.7897C3.73296 13.6129 3.47435 13.3593 3.2917 13.0534C3.10906 12.7475 3.00858 12.3996 3 12.0435V7.49437C3.0049 7.13344 3.10356 6.77997 3.28631 6.4686C3.46906 6.15723 3.72964 5.89865 4.0425 5.7182L7.9575 3.46988C8.27396 3.28554 8.6337 3.18841 9 3.18841C9.3663 3.18841 9.72604 3.28554 10.0425 3.46988V3.46239ZM6.75 12.0285H11.25V7.53184H6.75V12.0285Z" fill="url(#paint0_linear_81_2209)"/>
</g>
<defs>
<linearGradient id="paint0_linear_81_2209" x1="8.91" y1="0.86558" x2="8.91" y2="19.8391" gradientUnits="userSpaceOnUse">
<stop stop-color="#EECF56"/>
<stop offset="0.59" stop-color="#B79F41"/>
<stop offset="1" stop-color="#ECCF5F"/>
</linearGradient>
<clipPath id="clip0_81_2209">
<rect width="18" height="19.5604" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,11 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0131 12.5241L17.0131 12.5264C17.0135 12.7889 17.0135 14.4169 17.0134 15.908C17.0133 16.479 16.5099 16.942 15.8885 16.942H3.51276C2.8925 16.942 2.38912 16.4799 2.38902 15.9077L2.38844 12.6499C2.38913 12.6317 2.38948 12.6135 2.38948 12.5952L2.38924 6.33235C2.39309 6.03102 2.39299 4.48837 2.39211 3.0815C2.39174 2.50899 2.89615 2.0452 3.51799 2.04518L15.8881 2.04515C16.5094 2.04517 17.0131 2.50829 17.0131 3.07957L17.0131 12.5241ZM0.701979 12.5953L0.701736 6.32266C0.705532 6.06549 0.705512 4.51815 0.704606 3.0824C0.703704 1.65297 1.96337 0.493575 3.51797 0.493556L15.8881 0.49353C17.4414 0.493542 18.7006 1.65135 18.7006 3.07957V12.5241C18.701 12.7882 18.701 14.4182 18.7009 15.9081C18.7007 17.3362 17.4416 18.4936 15.8885 18.4936H3.51276C1.95964 18.4936 0.701773 17.3361 0.701518 15.908L0.700928 12.5976L0.701453 12.5965L0.701979 12.5953ZM14.2007 3.68748L9.70098 3.68744L5.20314 3.6875C4.58125 3.6875 4.07734 4.19149 4.07786 4.81338C4.0787 5.8114 4.07925 7.13977 4.07643 7.33068L4.07658 11.2515C4.07658 11.2517 4.07644 11.2518 4.07626 11.2518C4.07607 11.2518 4.07593 11.252 4.07593 11.2521L4.07638 13.8127C4.07649 14.434 4.5785 14.9375 5.19974 14.9375H9.70107H14.2009C14.8221 14.9375 15.3258 14.4343 15.3258 13.8131C15.3259 12.7871 15.326 11.4015 15.3257 11.2066V4.81248C15.3257 4.19116 14.822 3.68748 14.2007 3.68748ZM8.71683 11.2389C8.09551 11.2389 7.59183 10.7352 7.59183 10.1139V8.48798C7.59183 7.86666 8.09551 7.36298 8.71683 7.36298H10.6853C11.3066 7.36298 11.8103 7.86666 11.8103 8.48798V10.1139C11.8103 10.7352 11.3066 11.2389 10.6853 11.2389H8.71683Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0131 12.5241L17.0131 12.5264C17.0135 12.7889 17.0135 14.4169 17.0134 15.908C17.0133 16.479 16.5099 16.942 15.8885 16.942H3.51276C2.8925 16.942 2.38912 16.4799 2.38902 15.9077L2.38844 12.6499C2.38913 12.6317 2.38948 12.6135 2.38948 12.5952L2.38924 6.33235C2.39309 6.03102 2.39299 4.48837 2.39211 3.0815C2.39174 2.50899 2.89615 2.0452 3.51799 2.04518L15.8881 2.04515C16.5094 2.04517 17.0131 2.50829 17.0131 3.07957L17.0131 12.5241ZM0.701979 12.5953L0.701736 6.32266C0.705532 6.06549 0.705512 4.51815 0.704606 3.0824C0.703704 1.65297 1.96337 0.493575 3.51797 0.493556L15.8881 0.49353C17.4414 0.493542 18.7006 1.65135 18.7006 3.07957V12.5241C18.701 12.7882 18.701 14.4182 18.7009 15.9081C18.7007 17.3362 17.4416 18.4936 15.8885 18.4936H3.51276C1.95964 18.4936 0.701773 17.3361 0.701518 15.908L0.700928 12.5976L0.701453 12.5965L0.701979 12.5953ZM14.2007 3.68748L9.70098 3.68744L5.20314 3.6875C4.58125 3.6875 4.07734 4.19149 4.07786 4.81338C4.0787 5.8114 4.07925 7.13977 4.07643 7.33068L4.07658 11.2515C4.07658 11.2517 4.07644 11.2518 4.07626 11.2518C4.07607 11.2518 4.07593 11.252 4.07593 11.2521L4.07638 13.8127C4.07649 14.434 4.5785 14.9375 5.19974 14.9375H9.70107H14.2009C14.8221 14.9375 15.3258 14.4343 15.3258 13.8131C15.3259 12.7871 15.326 11.4015 15.3257 11.2066V4.81248C15.3257 4.19116 14.822 3.68748 14.2007 3.68748ZM8.71683 11.2389C8.09551 11.2389 7.59183 10.7352 7.59183 10.1139V8.48798C7.59183 7.86666 8.09551 7.36298 8.71683 7.36298H10.6853C11.3066 7.36298 11.8103 7.86666 11.8103 8.48798V10.1139C11.8103 10.7352 11.3066 11.2389 10.6853 11.2389H8.71683Z" fill="url(#paint0_linear_86_317)"/>
<defs>
<linearGradient id="paint0_linear_86_317" x1="9.61093" y1="1.28805" x2="9.61093" y2="18.7304" gradientUnits="userSpaceOnUse">
<stop stop-color="#42BB47"/>
<stop offset="0.59" stop-color="#33A137"/>
<stop offset="1" stop-color="#42BB47"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1,9 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 2H5.5C4.94772 2 4.5 2.44772 4.5 3V21C4.5 21.5523 4.94772 22 5.5 22H19.5C20.0523 22 20.5 21.5523 20.5 21V3C20.5 2.44772 20.0523 2 19.5 2Z" stroke="#42BB47" stroke-width="1.5"/>
<path d="M11 7H17" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M11 12H17" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path d="M11 17H17" stroke="#42BB47" stroke-width="1.5" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 8C8.5523 8 9 7.5523 9 7C9 6.4477 8.5523 6 8 6C7.4477 6 7 6.4477 7 7C7 7.5523 7.4477 8 8 8Z" fill="#42BB47"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 13C8.5523 13 9 12.5523 9 12C9 11.4477 8.5523 11 8 11C7.4477 11 7 11.4477 7 12C7 12.5523 7.4477 13 8 13Z" fill="#42BB47"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 18C8.5523 18 9 17.5523 9 17C9 16.4477 8.5523 16 8 16C7.4477 16 7 16.4477 7 17C7 17.5523 7.4477 18 8 18Z" fill="#42BB47"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,9 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.375 2.89563H5.375C4.82272 2.89563 4.375 3.34335 4.375 3.89563V21.8956C4.375 22.4479 4.82272 22.8956 5.375 22.8956H19.375C19.9273 22.8956 20.375 22.4479 20.375 21.8956V3.89563C20.375 3.34335 19.9273 2.89563 19.375 2.89563Z" stroke="#BBBBBB" stroke-width="1.5"/>
<path d="M10.875 7.89563H16.875" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M10.875 12.8956H16.875" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path d="M10.875 17.8956H16.875" stroke="#BBBBBB" stroke-width="1.5" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.875 8.89563C8.4273 8.89563 8.875 8.44793 8.875 7.89563C8.875 7.34333 8.4273 6.89563 7.875 6.89563C7.3227 6.89563 6.875 7.34333 6.875 7.89563C6.875 8.44793 7.3227 8.89563 7.875 8.89563Z" fill="#BBBBBB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.875 13.8956C8.4273 13.8956 8.875 13.4479 8.875 12.8956C8.875 12.3433 8.4273 11.8956 7.875 11.8956C7.3227 11.8956 6.875 12.3433 6.875 12.8956C6.875 13.4479 7.3227 13.8956 7.875 13.8956Z" fill="#BBBBBB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.875 18.8956C8.4273 18.8956 8.875 18.4479 8.875 17.8956C8.875 17.3433 8.4273 16.8956 7.875 16.8956C7.3227 16.8956 6.875 17.3433 6.875 17.8956C6.875 18.4479 7.3227 18.8956 7.875 18.8956Z" fill="#BBBBBB"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 256 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 MiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Some files were not shown because too many files have changed in this diff Show More