├── .eslintrc.json ├── src ├── pages │ └── home │ │ ├── index.ts │ │ ├── ui │ │ ├── main-page-info.tsx │ │ ├── main-actions.tsx │ │ └── layout.tsx │ │ ├── home.page.tsx │ │ └── home.page copy.tsx ├── app │ ├── globals.css │ ├── index.ts │ ├── theme.ts │ ├── create-emotion-cache.ts │ └── app.tsx ├── features │ ├── suggestions-list │ │ ├── index.ts │ │ ├── ui │ │ │ ├── list-layout.tsx │ │ │ ├── filters-layout.tsx │ │ │ └── suggestion-card.tsx │ │ ├── compose │ │ │ ├── suggestions-provider.tsx │ │ │ ├── suggestions-list.tsx │ │ │ └── suggestions-filters.tsx │ │ ├── model │ │ │ ├── entities.ts │ │ │ ├── suggestions-list.model.ts │ │ │ └── filters.model.tsx │ │ └── constants.ts │ └── update-suggestion-rating │ │ └── compose │ │ └── update-rating-buttons.tsx └── shared │ └── ui │ └── form │ ├── ui-text-field.tsx │ └── ui-select-field.tsx ├── pages ├── _app.tsx ├── index.tsx ├── api │ └── hello.ts └── _document.tsx ├── bun.lockb ├── public ├── favicon.ico ├── vercel.svg └── next.svg ├── next.config.js ├── .gitignore ├── package.json ├── tsconfig.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/home/index.ts: -------------------------------------------------------------------------------- 1 | export { HomePage } from "./home.page"; 2 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "@/app"; 2 | 3 | export default App; 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvgenyParomov/suggestions-frontend/HEAD/bun.lockb -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { HomePage } from "@/pages/home"; 2 | 3 | export default HomePage; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvgenyParomov/suggestions-frontend/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "@fontsource/roboto/300.css"; 2 | @import "@fontsource/roboto/400.css"; 3 | @import "@fontsource/roboto/500.css"; 4 | @import "@fontsource/roboto/700.css"; 5 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from "./app"; 2 | export type { AppProps } from "./app"; 3 | export { theme, roboto } from "./theme"; 4 | export { createEmotionCache } from "./create-emotion-cache"; 5 | -------------------------------------------------------------------------------- /src/features/suggestions-list/index.ts: -------------------------------------------------------------------------------- 1 | export { SuggestionsList } from "./compose/suggestions-list"; 2 | export { SuggestionsFilters } from "./compose/suggestions-filters"; 3 | export { SuggestionsProvider } from "./compose/suggestions-provider"; 4 | -------------------------------------------------------------------------------- /src/features/suggestions-list/ui/list-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "@mui/material"; 2 | import { ReactNode } from "react"; 3 | 4 | export function ListLayout({children}: { children: ReactNode }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/home/ui/main-page-info.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@mui/material"; 2 | 3 | export function MainPageInfo() { 4 | return ( 5 | 6 | Список предложений 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/features/suggestions-list/ui/filters-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "@mui/material"; 2 | import { ReactNode } from "react"; 3 | 4 | export function FiltersLayout({children}: { children: ReactNode }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/home/ui/main-actions.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button } from "@mui/material"; 2 | 3 | export function MainActions() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/features/suggestions-list/compose/suggestions-provider.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { FiltersDataProvider } from "../model/filters.model"; 3 | 4 | export function SuggestionsProvider({ children }: { children: ReactNode }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /src/features/suggestions-list/model/entities.ts: -------------------------------------------------------------------------------- 1 | export type TagId = number; 2 | export type StatusId = number; 3 | 4 | export type TagEntity = { 5 | id: number; 6 | label: string; 7 | }; 8 | 9 | export type StatusEntity = { 10 | id: number; 11 | label: string; 12 | }; 13 | 14 | export type SuggestionEntity = { 15 | id: number; 16 | title: string; 17 | description: string; 18 | tags: TagEntity[]; 19 | status: StatusEntity; 20 | createdAt: Date; 21 | grade: number; 22 | }; 23 | -------------------------------------------------------------------------------- /src/features/suggestions-list/compose/suggestions-list.tsx: -------------------------------------------------------------------------------- 1 | import { useSuggestionsList } from "../model/suggestions-list.model"; 2 | import { ListLayout } from "../ui/list-layout"; 3 | import { SuggestionCard } from "../ui/suggestion-card"; 4 | 5 | export function SuggestionsList() { 6 | const list = useSuggestionsList(); 7 | 8 | return ( 9 | 10 | {list.map((suggestion) => ( 11 | 12 | ))} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/shared/ui/form/ui-text-field.tsx: -------------------------------------------------------------------------------- 1 | import { TextField } from "@mui/material"; 2 | import { ChangeEventHandler } from "react"; 3 | 4 | export function UiTextField({ 5 | label, 6 | onChange, 7 | value, 8 | }: { 9 | label: string; 10 | value: string; 11 | onChange: (value: string) => void; 12 | }) { 13 | const handleChange: ChangeEventHandler = (event) => { 14 | onChange(event.target.value); 15 | }; 16 | return ( 17 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/home/home.page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SuggestionsFilters, 3 | SuggestionsList, 4 | SuggestionsProvider, 5 | } from "@/features/suggestions-list"; 6 | import { Layout } from "./ui/layout"; 7 | import { MainActions } from "./ui/main-actions"; 8 | import { MainPageInfo } from "./ui/main-page-info"; 9 | 10 | export function HomePage() { 11 | return ( 12 | 13 | } 15 | mainPageInfo={} 16 | mainContent={} 17 | sidebar={} 18 | /> 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/features/suggestions-list/constants.ts: -------------------------------------------------------------------------------- 1 | import { SuggestionOrder } from "./model/filters.model"; 2 | 3 | export const tagsOptions = [ 4 | { id: undefined, label: "Без тега" }, 5 | { id: 1, label: "Тег1" }, 6 | { id: 2, label: "Тег2" }, 7 | { id: 3, label: "Тег3" }, 8 | ]; 9 | 10 | export const statusOptions = [ 11 | { id: undefined, label: "Любой" }, 12 | { id: 1, label: "Открыто" }, 13 | { id: 2, label: "Закрыто" }, 14 | ]; 15 | 16 | export const sortOptions = [ 17 | { id: "createAt-desc", label: "По дате создания" }, 18 | { id: "grade-desc", label: "По пулярности" }, 19 | ] satisfies Array<{ id: SuggestionOrder; label: string }>; 20 | -------------------------------------------------------------------------------- /src/app/theme.ts: -------------------------------------------------------------------------------- 1 | import { Roboto } from "next/font/google"; 2 | import { createTheme } from "@mui/material/styles"; 3 | import { red } from "@mui/material/colors"; 4 | 5 | export const roboto = Roboto({ 6 | weight: ["300", "400", "500", "700"], 7 | subsets: ["cyrillic", "latin"], 8 | display: "swap", 9 | }); 10 | 11 | // Create a theme instance. 12 | export const theme = createTheme({ 13 | palette: { 14 | primary: { 15 | main: "#9351FF", 16 | }, 17 | secondary: { 18 | main: "#FFE93F", 19 | }, 20 | error: { 21 | main: red.A400, 22 | }, 23 | }, 24 | typography: { 25 | fontFamily: roboto.style.fontFamily, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/create-emotion-cache.ts: -------------------------------------------------------------------------------- 1 | import createCache from "@emotion/cache"; 2 | 3 | const isBrowser = typeof document !== "undefined"; 4 | 5 | // On the client side, Create a meta tag at the top of the and set it as insertionPoint. 6 | // This assures that MUI styles are loaded first. 7 | // It allows developers to easily override MUI styles with other styling solutions, like CSS modules. 8 | export function createEmotionCache() { 9 | let insertionPoint; 10 | 11 | if (isBrowser) { 12 | const emotionInsertionPoint = document.querySelector( 13 | 'meta[name="emotion-insertion-point"]' 14 | ); 15 | insertionPoint = emotionInsertionPoint ?? undefined; 16 | } 17 | 18 | return createCache({ key: "mui-style", insertionPoint }); 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.11.1", 13 | "@emotion/server": "^11.11.0", 14 | "@emotion/styled": "^11.11.0", 15 | "@fontsource/roboto": "^5.0.8", 16 | "@mui/icons-material": "^5.14.8", 17 | "@mui/material": "^5.14.8", 18 | "@types/node": "20.6.0", 19 | "@types/react": "18.2.21", 20 | "@types/react-dom": "18.2.7", 21 | "eslint": "8.49.0", 22 | "eslint-config-next": "13.4.19", 23 | "next": "13.4.19", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "typescript": "5.2.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/features/update-suggestion-rating/compose/update-rating-buttons.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Badge, ButtonGroup, Button } from "@mui/material"; 2 | import GradeIcon from "@mui/icons-material/Grade"; 3 | import AddIcon from "@mui/icons-material/Add"; 4 | import RemoveIcon from "@mui/icons-material/Remove"; 5 | 6 | export function UpdateRatingButtons() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "paths": { 21 | "@/*": [ 22 | "./src/*" 23 | ] 24 | }, 25 | "plugins": [ 26 | { 27 | "name": "next" 28 | } 29 | ] 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/features/suggestions-list/ui/suggestion-card.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | Typography, 5 | } from "@mui/material"; 6 | 7 | type Tag = { 8 | id: number; 9 | label: string; 10 | }; 11 | 12 | type Status = { 13 | id: number; 14 | label: string; 15 | }; 16 | 17 | export function SuggestionCard({ 18 | title, 19 | description, 20 | tags, 21 | status, 22 | }: { 23 | title: string; 24 | description: string; 25 | tags: Tag[]; 26 | status: Status; 27 | }) { 28 | return ( 29 | 30 | 31 | {title} 32 | {description} 33 | Теги: {tags.map((v) => v.label).join(", ")} 34 | Статус: {status.label} 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/pages/home/ui/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Box, Button, Typography } from "@mui/material"; 2 | import { ReactNode } from "react"; 3 | 4 | export function Layout({ 5 | actions, 6 | mainPageInfo, 7 | mainContent, 8 | sidebar 9 | }: { 10 | actions: ReactNode; 11 | mainPageInfo: ReactNode; 12 | sidebar: ReactNode; 13 | mainContent: ReactNode; 14 | }) { 15 | return ( 16 | 17 | 18 | 19 | {actions} 20 | 21 | {mainPageInfo} 22 | 23 | 24 | {sidebar} 25 | {mainContent} 26 | 27 | 28 | ); 29 | } 30 | 31 | 32 | 33 | ; 34 | 35 | 36 | Список предложений 37 | ; 38 | -------------------------------------------------------------------------------- /src/shared/ui/form/ui-select-field.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, InputLabel, Select, MenuItem } from "@mui/material"; 2 | 3 | export function UiSelectField< 4 | IdKey extends string, 5 | LabelKey extends string, 6 | Option extends Record & Record 7 | >({ 8 | idKey, 9 | label, 10 | labelKey, 11 | onChange, 12 | options, 13 | value, 14 | }: { 15 | label: string; 16 | idKey: IdKey; 17 | labelKey: LabelKey; 18 | value: Option[IdKey]; 19 | onChange: (value: Option[IdKey]) => void; 20 | options: Option[]; 21 | }) { 22 | return ( 23 | 24 | {label} 25 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/features/suggestions-list/model/suggestions-list.model.ts: -------------------------------------------------------------------------------- 1 | import { SuggestionEntity } from "./entities"; 2 | import { useFilters } from "./filters.model"; 3 | 4 | export function useSuggestionsList() { 5 | const data: SuggestionEntity[] = [ 6 | { 7 | id: 1, 8 | createdAt: new Date(), 9 | description: "Description", 10 | title: "title 1", 11 | grade: 4, 12 | status: { id: 1, label: "Открыто" }, 13 | tags: [{ id: 1, label: "Тег1" }], 14 | }, 15 | { 16 | id: 2, 17 | createdAt: new Date(Date.now() - 100000000), 18 | description: "Description", 19 | title: "title 2", 20 | grade: 6, 21 | status: { id: 2, label: "Закрыто" }, 22 | tags: [{ id: 2, label: "Тег2" }], 23 | }, 24 | ]; 25 | 26 | const { data: filtersData } = useFilters(); 27 | 28 | let preparedData = data; 29 | 30 | preparedData = preparedData.filter((item) => { 31 | if (filtersData.status && filtersData.status !== item.status.id) { 32 | return false; 33 | } 34 | 35 | return true; 36 | }); 37 | 38 | return preparedData; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Head from "next/head"; 3 | import { AppProps as NextAppProps } from "next/app"; 4 | import { ThemeProvider } from "@mui/material/styles"; 5 | import CssBaseline from "@mui/material/CssBaseline"; 6 | import { CacheProvider, EmotionCache } from "@emotion/react"; 7 | import { theme } from "./theme"; 8 | import { createEmotionCache } from "./create-emotion-cache"; 9 | 10 | // Client-side cache, shared for the whole session of the user in the browser. 11 | const clientSideEmotionCache = createEmotionCache(); 12 | 13 | export interface AppProps extends NextAppProps { 14 | emotionCache?: EmotionCache; 15 | } 16 | 17 | export function App(props: AppProps) { 18 | const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/features/suggestions-list/compose/suggestions-filters.tsx: -------------------------------------------------------------------------------- 1 | import { UiSelectField } from "@/shared/ui/form/ui-select-field"; 2 | import { UiTextField } from "@/shared/ui/form/ui-text-field"; 3 | import { FiltersLayout } from "../ui/filters-layout"; 4 | import { useFilters } from "../model/filters.model"; 5 | import { sortOptions, statusOptions, tagsOptions } from "../constants"; 6 | 7 | export function SuggestionsFilters() { 8 | const { data, updateOrder, updateQuery, updateStatus, updateTag } = 9 | useFilters(); 10 | 11 | return ( 12 | 13 | 18 | 26 | 34 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/features/suggestions-list/model/filters.model.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dispatch, 3 | ReactNode, 4 | SetStateAction, 5 | createContext, 6 | useContext, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | import { StatusId, TagId } from "./entities"; 11 | 12 | export type SuggestionOrder = "grade-desc" | "createAt-desc"; 13 | 14 | export type SuggestionFiltersData = { 15 | query: string; 16 | tag: TagId | undefined; 17 | status: StatusId | undefined; 18 | order: SuggestionOrder; 19 | }; 20 | 21 | const filtersContext = createContext<{ 22 | data: SuggestionFiltersData; 23 | setData: Dispatch>; 24 | } | null>(null); 25 | 26 | export function FiltersDataProvider({ children }: { children: ReactNode }) { 27 | const [data, setData] = useState({ 28 | order: 'createAt-desc', 29 | query: "", 30 | status: undefined, 31 | tag: undefined, 32 | }); 33 | return ( 34 | ({ data, setData }), [data])}> 35 | {children} 36 | 37 | ); 38 | } 39 | 40 | export function useFilters() { 41 | const contextValue = useContext(filtersContext); 42 | if (!contextValue) throw new Error("context not provided"); 43 | 44 | const { data, setData } = contextValue; 45 | 46 | const updateQuery = (query: string) => { 47 | setData((d) => ({ ...d, query })); 48 | }; 49 | const updateTag = (tag: TagId | undefined) => { 50 | setData((d) => ({ ...d, tag })); 51 | }; 52 | const updateStatus = (status: StatusId | undefined) => { 53 | setData((d) => ({ ...d, status })); 54 | }; 55 | const updateOrder = (order: SuggestionOrder) => { 56 | setData((d) => ({ ...d, order })); 57 | }; 58 | 59 | return { 60 | data, 61 | updateStatus, 62 | updateTag, 63 | updateQuery, 64 | updateOrder, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | 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. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /src/pages/home/home.page copy.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEventHandler, useState } from "react"; 2 | import { 3 | Container, 4 | TextField, 5 | FormControl, 6 | InputLabel, 7 | Select, 8 | MenuItem, 9 | Grid, 10 | Typography, 11 | SelectChangeEvent, 12 | Card, 13 | CardContent, 14 | Box, 15 | Badge, 16 | Button, 17 | ButtonGroup, 18 | Stack, 19 | } from "@mui/material"; 20 | 21 | const suggestionsData = [ 22 | { 23 | id: 1, 24 | title: "Предложение 1", 25 | description: "Это предложение 1", 26 | tags: [1, 2], 27 | status: 1, 28 | popularity: 10, 29 | }, 30 | { 31 | id: 2, 32 | title: "Предложение 2", 33 | description: "Это предложение 2", 34 | tags: [3], 35 | status: 2, 36 | popularity: 5, 37 | }, 38 | ]; 39 | 40 | const tagsOptions = [ 41 | { id: 1, label: "Тег1" }, 42 | { id: 2, label: "Тег2" }, 43 | { id: 3, label: "Тег3" }, 44 | ]; 45 | 46 | const statusOptions = [ 47 | { id: 1, label: "Открыто" }, 48 | { id: 2, label: "Закрыто" }, 49 | ]; 50 | 51 | const sortOptions = [ 52 | { id: "популярности", label: "Популярности" }, 53 | { id: "другой-вариант", label: "Другой вариант сортировки" }, 54 | ]; 55 | 56 | export function HomePage() { 57 | const [filterTags, setFilterTags] = useState(""); 58 | const [filterStatus, setFilterStatus] = useState(""); 59 | const [sortType, setSortType] = useState("популярности"); 60 | const [searchQuery, setSearchQuery] = useState(""); 61 | 62 | const handleFilterTagsChange = (event: SelectChangeEvent) => { 63 | setFilterTags(event.target.value); 64 | }; 65 | 66 | const handleFilterStatusChange = (event: SelectChangeEvent) => { 67 | setFilterStatus(event.target.value); 68 | }; 69 | 70 | const handleSortChange = (event: SelectChangeEvent) => { 71 | setSortType(event.target.value); 72 | }; 73 | 74 | const handleSearchChange: ChangeEventHandler = (event) => { 75 | setSearchQuery(event.target.value); 76 | }; 77 | 78 | const filteredSuggestions = suggestionsData.filter((suggestion) => { 79 | return true; 80 | }); 81 | 82 | filteredSuggestions.sort((a, b) => { 83 | if (sortType === "популярности") { 84 | return b.popularity - a.popularity; 85 | } 86 | return 0; 87 | }); 88 | 89 | return ( 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Список предложений 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Document, { 3 | Html, 4 | Head, 5 | Main, 6 | NextScript, 7 | DocumentProps, 8 | DocumentContext, 9 | } from "next/document"; 10 | import createEmotionServer from "@emotion/server/create-instance"; 11 | import { AppType } from "next/app"; 12 | import { AppProps, theme, roboto, createEmotionCache } from "@/app"; 13 | 14 | interface MyDocumentProps extends DocumentProps { 15 | emotionStyleTags: JSX.Element[]; 16 | } 17 | 18 | export default function MyDocument({ emotionStyleTags }: MyDocumentProps) { 19 | return ( 20 | 21 | 22 | {/* PWA primary color */} 23 | 24 | 25 | 26 | {emotionStyleTags} 27 | 28 | 29 |
30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | // `getInitialProps` belongs to `_document` (instead of `_app`), 37 | // it's compatible with static-site generation (SSG). 38 | MyDocument.getInitialProps = async (ctx: DocumentContext) => { 39 | // Resolution order 40 | // 41 | // On the server: 42 | // 1. app.getInitialProps 43 | // 2. page.getInitialProps 44 | // 3. document.getInitialProps 45 | // 4. app.render 46 | // 5. page.render 47 | // 6. document.render 48 | // 49 | // On the server with error: 50 | // 1. document.getInitialProps 51 | // 2. app.render 52 | // 3. page.render 53 | // 4. document.render 54 | // 55 | // On the client 56 | // 1. app.getInitialProps 57 | // 2. page.getInitialProps 58 | // 3. app.render 59 | // 4. page.render 60 | 61 | const originalRenderPage = ctx.renderPage; 62 | 63 | // You can consider sharing the same Emotion cache between all the SSR requests to speed up performance. 64 | // However, be aware that it can have global side effects. 65 | const cache = createEmotionCache(); 66 | const { extractCriticalToChunks } = createEmotionServer(cache); 67 | 68 | ctx.renderPage = () => 69 | originalRenderPage({ 70 | enhanceApp: ( 71 | App: React.ComponentType & AppProps> 72 | ) => 73 | function EnhanceApp(props) { 74 | return ; 75 | }, 76 | }); 77 | 78 | const initialProps = await Document.getInitialProps(ctx); 79 | // This is important. It prevents Emotion to render invalid HTML. 80 | // See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 81 | const emotionStyles = extractCriticalToChunks(initialProps.html); 82 | const emotionStyleTags = emotionStyles.styles.map((style: any) => ( 83 |