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
59 changes: 55 additions & 4 deletions api/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,32 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/v1/auth/me:
get:
tags: [auth]
summary: Get info of logged in user
operationId: userInfo
security:
- cookieAuth: []
responses:
'200':
description: Successfully got user info
content:
application/json:
schema:
$ref: '#/components/schemas/UserInfoResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/api/v1/user/query:
post:
tags: [user]
Expand Down Expand Up @@ -206,14 +232,16 @@ paths:
name: offset
schema:
type: integer
format: uint64
default: 0
required: true # TODO: проверить можно ли убрать
required: false
- in: query
name: limit
schema:
type: integer
format: uint64
default: 20
required: true
required: false
responses:
'200':
description: Successfully retrieved user search queries
Expand All @@ -229,6 +257,12 @@ paths:
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:
Expand Down Expand Up @@ -286,6 +320,10 @@ components:
type: string
minLength: 1
maxLength: 255
category:
type: string
minLength: 1
maxLength: 255
start:
type: string
format: date-time
Expand All @@ -298,11 +336,13 @@ components:
properties:
token:
type: string
category:
type: string
records:
type: array
items:
$ref: '#/components/schemas/TokenRecord'
required: [token, records]
required: [token, category, records]
TokenRecord:
type: object
properties:
Expand All @@ -317,10 +357,13 @@ components:
interest_normalized:
type: number
format: float64
interest_category:
type: number
format: float64
sentiment:
type: integer
format: int16
required: [interest, interest_normalized, sentiment]
required: [interest, interest_normalized, interest_category, sentiment]
required: [timestamp, features]
VkAuthCallbackRequest:
type: object
Expand Down Expand Up @@ -400,6 +443,14 @@ components:
type: string
format: date-time
required: [id, query, searchDate]
UserInfoResponse:
type: object
properties:
username:
type: string
email:
type: string
required: [username, email]

Error:
type: object
Expand Down
6 changes: 6 additions & 0 deletions app/entities/auth/api/me.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { apiRoutes } from '@app/shared/config/routes';
import { client } from '@shared/api/client';

export const getMe = async () => {
return client.GET(apiRoutes.getMe);
};
1 change: 1 addition & 0 deletions app/entities/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { getMe } from './api/me.api';
export { useUserStore } from './store/user.store';
28 changes: 27 additions & 1 deletion app/entities/auth/store/query.store.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface QueryItem {
id: string;
query: string;
}

interface SavedQueriesStore {
_items: Record<string, string>;

queries: QueryItem[];
setQueries: (queries: QueryItem[]) => void;

getQueryID: (query: string) => string | undefined;
isQuerySaved: (query: string) => boolean;
saveQuery: (id: string, query: string) => void;
Expand All @@ -15,6 +23,20 @@ export const useSavedQueriesStore = create<SavedQueriesStore>()(
(set, get) => ({
_items: {},

queries: [],
setQueries: (queries: QueryItem[]) => {
set({
queries,
_items: queries.reduce(
(acc, q) => {
acc[q.query] = q.id;
return acc;
},
{} as Record<string, string>,
),
});
},

getQueryID: (query: string) => {
return get()._items[query];
},
Expand All @@ -26,14 +48,18 @@ export const useSavedQueriesStore = create<SavedQueriesStore>()(
saveQuery: (id: string, query: string) => {
set((state) => ({
_items: { ...state._items, [query]: id },
queries: [...state.queries, { id, query }],
}));
},

removeQuery: (query: string) => {
set((state) => {
const newItems = { ...state._items };
delete newItems[query];
return { _items: newItems };
return {
_items: newItems,
queries: state.queries.filter((q) => q.query !== query),
};
});
},
}),
Expand Down
15 changes: 11 additions & 4 deletions app/entities/auth/store/user.store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

type AuthStatus = 'unknown' | 'authenticated' | 'guest';

export interface AuthUser {
vkid: number;
vkid?: number;
username: string;
email: string;
}

interface UserState {
status: AuthStatus;
user?: AuthUser;
pendingAuth?: {
username: string;
Expand All @@ -26,11 +29,15 @@ interface UserState {
export const useUserStore = create<UserState>()(
persist(
(set) => ({
status: 'unknown',
user: undefined,
pendingAuth: undefined,
setUser: (user) => set({ user, pendingAuth: undefined }),
setPendingAuth: (pending) => set({ pendingAuth: pending }),
logout: () => set({ user: undefined, pendingAuth: undefined }),
setUser: (user) =>
set({ status: 'authenticated', user, pendingAuth: undefined }),
setPendingAuth: (pending) =>
set({ status: 'guest', pendingAuth: pending }),
logout: () =>
set({ status: 'guest', user: undefined, pendingAuth: undefined }),
}),
{ name: 'user' },
),
Expand Down
9 changes: 3 additions & 6 deletions app/features/search-token-query/model/eval-query.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,15 @@ const fillMissingDates = (

export const executeQuerySearch = async (
query: string,
start?: string,
): Promise<SearchResult | { error: string }> => {
try {
const tokens = extractTokens(query);
if (tokens.length === 0) {
const { data, error } = await POST(apiRoutes.searchToken, {
body: {
token: query,
start: new Date(
new Date().setFullYear(new Date().getFullYear() - 1),
).toISOString(),
start: start,
} as SearchTokenInfoRequest,
});
if (error !== undefined) {
Expand All @@ -132,9 +131,7 @@ export const executeQuerySearch = async (
POST(apiRoutes.searchToken, {
body: {
token: token,
start: new Date(
new Date().setFullYear(new Date().getFullYear() - 1),
).toISOString(),
start: start,
} as SearchTokenInfoRequest,
}),
);
Expand Down
40 changes: 40 additions & 0 deletions app/features/search-token-query/ui/filter-category.ui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useQueryParam } from '@shared/lib/hooks/use-query-param';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@shared/ui/select';

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

export const FilterCategory = () => {
const [category, setCategory] = useQueryParam('category');

return (
<div>
<Select value={category ?? ''} onValueChange={setCategory}>
<SelectTrigger className="w-full text-xs text-muted-foreground">
<SelectValue placeholder="Выберите категорию" />
</SelectTrigger>

<SelectContent>
{CATEGORIES.map((item) => (
<SelectItem
key={item.value}
value={item.value}
className="text-xs text-muted-foreground"
>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
32 changes: 32 additions & 0 deletions app/features/search-token-query/ui/filters.ui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';

import { cn } from '@app/shared/lib/utils';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@shared/ui/accordion';

type SearchTokenFiltersProps = {
children: React.ReactNode;
className?: string;
};

export const SearchFilters = ({
children,
className,
}: SearchTokenFiltersProps) => {
return (
<Accordion type="single" collapsible className={cn('w-full', className)}>
<AccordionItem value="filters">
<AccordionTrigger className="text-xs text-muted-foreground pb-2">
Расширенные параметры
</AccordionTrigger>
<AccordionContent>
<div>{children}</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
39 changes: 24 additions & 15 deletions app/features/search-token-query/ui/search-token-query.ui.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React, { useEffect, useState } from 'react';
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 { Input } from '@shared/ui/input';
import { Spinner } from '@shared/ui/spinner';

import { FilterCategory } from './filter-category.ui';
import { SearchFilters } from './filters.ui';

export const SearchTokenQuery = () => {
const [searchParams] = useSearchParams();
const [query, setQuery] = useState(searchParams.get('query') || '');
Expand Down Expand Up @@ -36,7 +39,7 @@ export const SearchTokenQuery = () => {
}

const formattedQuery = formatQuery(query);
const params = new URLSearchParams();
const params = new URLSearchParams(searchParams);
params.set('query', formattedQuery);

navigate({
Expand All @@ -46,19 +49,25 @@ export const SearchTokenQuery = () => {
};

return (
<form onSubmit={onSubmit} className="flex w-full items-center gap-2">
<Input
type="search"
placeholder="Токен или формула"
value={query}
onChange={(e) => setQuery(e.target.value)}
required
className="flex-1"
aria-label="Поиск по токену или формуле"
/>
<Button type="submit" disabled={isLoading} className="w-[100px]">
{isLoading ? <Spinner /> : 'Поиск'}
</Button>
<form onSubmit={onSubmit} className="w-full">
<div className="flex items-center gap-2">
<InputWithTooltip
type="search"
placeholder="Токен или формула"
tooltip="Можно вводить токены (например, vk) или формулы вида token('vk') / token('vixar') * 100 с операторами +, -, *, /"
value={query}
onChange={(e) => setQuery(e.target.value)}
required
className="flex-1"
/>
<Button type="submit" disabled={isLoading} className="w-[100px]">
{isLoading ? <Spinner /> : 'Поиск'}
</Button>
</div>

<SearchFilters>
<FilterCategory />
</SearchFilters>
</form>
);
};
Loading