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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions api/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,133 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/v1/user/subs/token:
post:
tags: [user]
summary: Subscribe user to specified token
operationId: subscribeUserToToken
security:
- cookieAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SubscribeUserToTokenRequest'
responses:
'200':
description: Successfully saved user search query
content:
application/json:
schema:
$ref: '#/components/schemas/SubscribeUserToTokenResponse'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: Conflict - already subscribed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
get:
tags: [user]
summary: Get user's token subs
operationId: getUserTokenSubs
security:
- cookieAuth: []
parameters:
- in: query
name: offset
schema:
type: integer
format: uint64
default: 0
required: false
- in: query
name: limit
schema:
type: integer
format: uint64
default: 20
required: false
responses:
'200':
description: Successfully retrieved user's token subs
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserTokenSub'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
tags: [user]
summary: Delete user's token subscription
operationId: deleteUserTokenSub
security:
- cookieAuth: []
parameters:
- in: query
name: id
schema:
type: string
minLength: 16
maxLength: 128
required: true
responses:
'200':
description: Successfully deleted user's token subscription
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

components:
securitySchemes:
Expand Down Expand Up @@ -451,6 +578,51 @@ components:
email:
type: string
required: [username, email]
SubscribeUserToTokenRequest:
type: object
properties:
token:
type: string
minLength: 1
maxLength: 128
category:
type: string
minLength: 1
maxLength: 128
threshold:
type: number
method:
type: string
minLength: 1
maxLength: 128
required: [token, category, threshold]
SubscribeUserToTokenResponse:
type: object
properties:
id:
type: string
required: [id]
UserTokenSub:
type: object
properties:
id:
type: string
token:
type: string
category:
type: string
method:
type: string
current_interest:
type: number
format: float64
previous_interest:
type: number
format: float64
last_scan:
type: string
format: date-time
required: [id, token, category, method, current_interest, previous_interest, last_scan]

Error:
type: object
Expand Down
5 changes: 5 additions & 0 deletions app/entities/category/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
categories,
categoryOptions,
mapCategoryToLabel,
} from './lib/categories.lib';
18 changes: 18 additions & 0 deletions app/entities/category/lib/categories.lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const categories = {
news: 'Новости',
marketplace: 'Маркетплейсы',
review: 'Отзывы',
} as const;

type Category = keyof typeof categories;

export const mapCategoryToLabel = (category: Category) => {
return categories[category];
};

export const categoryOptions = (Object.keys(categories) as Category[]).map(
(key) => ({
value: key,
label: categories[key],
}),
);
1 change: 1 addition & 0 deletions app/entities/method/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { mapMethodToLabel, methodOptions, methods } from './lib/methods.lib';
18 changes: 18 additions & 0 deletions app/entities/method/lib/methods.lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { UserTokenSub } from '@app/shared/api';

export const methods: Record<UserTokenSub['method'], string> = {
denormalized: 'Ненормированный интерес',
global_median: 'Нормирование глобальной медианой',
category_median: 'Нормирование медианой категории',
} as const;

type Method = keyof typeof methods;

export const mapMethodToLabel = (method: Method) => {
return methods[method];
};

export const methodOptions = (Object.keys(methods) as Method[]).map((key) => ({
value: key,
label: methods[key],
}));
1 change: 1 addition & 0 deletions app/entities/token-subscription/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useTokenSubscriptionsStore } from './store/token-subscription.store';
101 changes: 101 additions & 0 deletions app/entities/token-subscription/store/token-subscription.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface TokenSubscriptionItem {
id: string;
token: string;
category: string;
method: string;
current_interest: number;
previous_interest: number;
last_scan: string;
}

interface TokenSubscriptionsStore {
_items: Record<string, string>;
subscriptions: TokenSubscriptionItem[];

setSubscriptions: (subscriptions: TokenSubscriptionItem[]) => void;
isTokenSubscribed: (token: string) => boolean;
getSubscriptionID: (token: string) => string | undefined;
subscribe: (
id: string,
token: string,
category: string,
method: string,
current_interest: number,
previous_interest: number,
last_scan: string,
) => void;
unsubscribe: (token: string) => void;
}

export const useTokenSubscriptionsStore = create<TokenSubscriptionsStore>()(
persist(
(set, get) => ({
_items: {},

subscriptions: [],
setSubscriptions: (subscriptions: TokenSubscriptionItem[]) => {
set({
subscriptions,
_items: subscriptions.reduce(
(acc, q) => {
acc[q.token] = q.id;
return acc;
},
{} as Record<string, string>,
),
});
},

getSubscriptionID: (token: string) => {
return get()._items[token];
},

isTokenSubscribed: (token: string) => {
return get()._items[token] !== undefined;
},

subscribe: (
id: string,
token: string,
category: string,
method: string,
current_interest: number,
previous_interest: number,
last_scan: string,
) => {
set((state) => ({
_items: { ...state._items, [token]: id },
subscriptions: [
...state.subscriptions,
{
id,
token,
category,
method,
current_interest,
previous_interest,
last_scan,
},
],
}));
},

unsubscribe: (token: string) => {
set((state) => {
const newItems = { ...state._items };
delete newItems[token];
return {
_items: newItems,
subscriptions: state.subscriptions.filter((q) => q.token !== token),
};
});
},
}),
{
name: 'token-subscriptions',
},
),
);
9 changes: 2 additions & 7 deletions app/features/search-token-query/ui/filter-category.ui.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router';

import { categoryOptions } from '@app/entities/category';
import {
Select,
SelectContent,
Expand All @@ -9,12 +10,6 @@
SelectValue,
} from '@shared/ui/select';

const CATEGORIES = [
{ value: 'news', label: 'Новости' },
{ value: 'marketplace', label: 'Маркетплейсы' },
{ value: 'review', label: 'Отзывы' },
];

export const FilterCategory = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [category, setCategory] = useState<string>(
Expand All @@ -24,7 +19,7 @@
useEffect(() => {
const urlCat = searchParams.get('category');
if (urlCat) setCategory(urlCat);
}, [searchParams.get('category')]);

Check warning on line 22 in app/features/search-token-query/ui/filter-category.ui.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked

Check warning on line 22 in app/features/search-token-query/ui/filter-category.ui.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has a missing dependency: 'searchParams'. Either include it or remove the dependency array

const handleChange = (val: string) => {
setCategory(val);
Expand All @@ -41,7 +36,7 @@
</SelectTrigger>

<SelectContent>
{CATEGORIES.map((item) => (
{categoryOptions.map((item) => (
<SelectItem
key={item.value}
value={item.value}
Expand Down
8 changes: 1 addition & 7 deletions app/features/search-token-query/ui/search-token-query.ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { useNavigate, useNavigation, useSearchParams } from 'react-router';

import { InputWithTooltip } from '@app/shared/ui/input-with-tooltip';
import { isQuery } from '@entities/token';
import { routes } from '@shared/config/routes';
import { Button } from '@shared/ui/button';
import { Spinner } from '@shared/ui/spinner';
Expand All @@ -21,16 +20,11 @@
useEffect(() => {
const urlQuery = searchParams.get('query');
setQuery(urlQuery ?? query);
}, [searchParams.get('query')]);

Check warning on line 23 in app/features/search-token-query/ui/search-token-query.ui.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked

Check warning on line 23 in app/features/search-token-query/ui/search-token-query.ui.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook useEffect has missing dependencies: 'query' and 'searchParams'. Either include them or remove the dependency array. You can also do a functional update 'setQuery(q => ...)' if you only need 'query' in the 'setQuery' call

const formatQuery = (input: string): string => {
const trimmed = input.trim();

if (isQuery(trimmed)) {
return trimmed;
} else {
return `token('${trimmed}')`;
}
return trimmed;
};

const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
Expand Down
1 change: 1 addition & 0 deletions app/features/token-subscribe-dialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TokenSubscribeDialog } from './ui/token-subscribe-dialog.ui';
Loading