-
Notifications
You must be signed in to change notification settings - Fork 0
Dev #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: code-review
Are you sure you want to change the base?
Dev #1
Conversation
Set up project structure with React, TypeScript, Vite, ESLint, Tailwind CSS, and essential providers, entities, shared utilities, and pages. Includes initial configuration files, dependencies, and README documentation.
Introduces a reusable ExpandableText component for handling long text with expand/collapse or navigation options. Integrates ExpandableText into HomePage and QuestionPage to improve display of question descriptions and answers. Also fixes a minor style property in CodeBlock.
Refactored HomePage, QuestionPage, CodeBlock, and ExpandableText components to use React.memo for improved rendering performance. Added useCallback and useMemo where appropriate to prevent unnecessary re-renders and optimize component behavior.
Enhanced password validation on the registration page to require at least one lowercase, one uppercase letter, one digit, and one symbol. Improved error handling in auth provider and registration page to provide more user-friendly messages for common API errors. Updated axios base URL logic to support Vite proxy and better CORS handling. Added vite-plugin-mkcert for local HTTPS development. Added empty Clamp.tsx and tailwind.config.ts files.
Moved header, home page, question details, answer item, and form UIs into dedicated presentational components for improved separation of concerns and reusability. Updated imports and routing to reflect new file structure. No functional changes, only refactoring for maintainability.
Moved Paginated and PaginatedMeta types from question and snippet modules to a new shared/types/pagination.ts file. Updated imports in related API files to use the shared pagination types for consistency and reusability.
This update introduces a toggle on the homepage to view either questions or code snippets, with unified card rendering via ItemCard. It adds a dedicated SnippetPage for viewing snippet details and comments, supports commenting on snippets, and refactors shared UI components for reuse. API and types for snippets are extended to handle comments, likes, and dislikes.
Introduces an account page with profile and password management, user statistics, and supporting UI components. Refactors home and snippet card views into dedicated components for questions and snippets, improving code organization and reusability. Adds user API and types for account-related features, and updates routing to include the account page.
Syncs the selected mode ('questions' or 'snippets') with the URL search params on HomePage and passes mode state during navigation. BackLink now restores the mode when returning to Home, improving navigation consistency.
Introduced a reusable Skeleton component for loading states and replaced text-based loading indicators with Skeleton placeholders across account, home, question, and snippet pages. This improves user experience by providing visual feedback during data fetching.
Moved the answer form on QuestionPage and the comment form on SnippetPage directly below the details section for improved UX. Adjusted skeleton loaders to reflect the new layout, and reordered the rendering of answers and comments accordingly.
Header is now sticky at the top of the page and its vertical padding changes based on scroll position for improved UX. Added useEffect and useState to track scroll position and update header style accordingly.
Introduces a reusable Avatar component that displays a colored emoji avatar based on username. Integrates Avatar into HeaderView, QuestionCardView, SnippetCardView, SnippetPage comments, and SnippetDetailsView to visually represent users throughout the UI.
Moved header scroll logic to a new Header container component and updated imports. Extracted answer and comment forms, and comments list into dedicated view components for better separation of concerns and reusability. Removed unused QuestionCard component and updated file structure for ItemCard. Updated HomePage, QuestionPage, and SnippetPage to use new view components.
Introduces a new CreatePage for questions and snippets, adds socket.io support for real-time snippet comments, and unifies question/snippet card views into ItemCommonCardView. Refactors code block and editor components for better language handling, memoizes several UI components, and updates API hooks for creating questions/snippets and marking answers. Removes legacy QuestionCardView and SnippetCardView components.
Introduces debug logging across AuthProvider, QuestionPage, and HTTP API layer, controlled by environment, query string, or localStorage. Enhances QuestionPage to restrict answer marking to question owners, supporting multiple API data shapes for owner identification.
Eliminated all debug-related flags, logging, and forced refresh logic from AuthProvider, QuestionPage, and HTTP API modules to clean up production code and improve maintainability.
Refactored AnswerItemView to use conditional class names for correct and normal answers, enhancing visual distinction. Updated the correct answer label to 'Верный ответ' with improved styling. Removed minor extraneous whitespace in auth and question page files.
Refactored Header to use requestAnimationFrame for scroll event handling and improved atTop state management with useRef. Updated HeaderView to use smoother transition timing and replaced username display with a profile link, removing redundant account link.
Implemented socket-based real-time updates for question answers and answer state changes. Added hooks and emitters for subscribing to and broadcasting answer creation and state changes in both frontend and backend. Refactored related logic in QuestionPage and SnippetPage to use new socket utilities.
Replaced complex owner ID and username extraction with direct access to the question's user property. This improves readability and maintainability by relying on the typed Question model.
Moved question, answer, snippet, and account form logic into dedicated hooks for better separation of concerns and reusability. Updated related pages to use these hooks, simplifying component code and improving maintainability. Added new index files for hooks and refactored socket event handling into hooks. Also introduced language normalization and validation service for snippet creation.
Added path aliases for src directories in tsconfig and Vite config. Refactored all relative imports to use the new alias paths for improved code maintainability and readability. Also removed the unused AccountPageNew.tsx file and updated PasswordFormView UI.
Introduces a new MyItemsPage for displaying user's questions and snippets, adds routing for '/my', and updates the header to include a navigation link to the new page.
Introduces hooks and UI for updating and deleting questions and snippets. Adds ownership checks, edit forms, and action buttons to QuestionPage and SnippetPage. Refactors details views to support custom action buttons for owners.
Introduced mutations and UI for editing and deleting answers on questions and comments on snippets. Updated relevant API hooks, page components, and list views to support inline editing and removal by content owners.
Introduces a new item details module consolidating question and snippet pages under src/pages/item/. Replaces legacy question/snippet detail pages and related UI components with unified, reusable views and hooks. Refactors all button usages to use the shared Button component for consistent styling and behavior across the app. Updates routing and props to support the new structure.
Removed in-memory and localStorage auth token management in favor of relying on httpOnly cookies. Updated answer form to use 'onChange' mode and improved controlled textarea handling. Enhanced account info view with avatar display. Simplified HTTP error normalization logic.
Removed generic ItemDetailsPage and useItemDetails in favor of dedicated question and snippet modules. Moved question and snippet related components and hooks into their respective subfolders for better separation. Updated imports and exports to reflect new structure, improving maintainability and clarity.
Implemented socket event handling and emission for updating and deleting answers and comments in both client and server code. This enables real-time synchronization of answer and comment changes across clients. Also improved development logging and CORS configuration for local HTTPS.
Introduces a notifications context and hooks for global app notifications. Refactors error handling to use a new AppError model, replacing legacy error conversion. Updates mutation hooks to emit notifications on success/error, and improves form error mapping. Removes unused CreatePageNew and legacy question details hook, consolidating logic. Enhances dark mode styles for create page forms.
Introduces a 'resolved' prop to ItemCommonCardView and displays a status badge on question cards based on whether the question is resolved or has a correct answer. Enhances user clarity on question status.
Refactored several components (ItemCard, ItemDetailsView, EditableAnswerItem, ActionButtons, CommentsListView) to use React.memo and useCallback/useMemo hooks for improved rendering performance and reduced unnecessary re-renders. This change enhances efficiency, especially for lists of answers and comments, and standardizes memoization usage across item-related UI components.
Inserted relevant UI screenshots into both English and Russian README files to visually illustrate key features. Added four new images showing questions, snippets, and code editor modes for improved documentation clarity.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| const [atTop, setAtTop] = useState(true); | ||
| const atTopRef = useRef<boolean>(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two sources of truth for the same piece of data. This creates complexity and potential for bugs if they get out of sync, also because of this you're now forced to keep both values in sync manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I was experimenting with this sticky header feature and forgot to clean-up the code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in f05daba
| <AuthContext.Provider | ||
| value={{ user, loading, login, logout, register, refresh }}> | ||
| {children} | ||
| </AuthContext.Provider> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React 19, you can just use the context as provider, check latest docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in bc0b028
| const raw = unwrapData<unknown>(res.data); | ||
| const normalized: User = { | ||
| id: Number((raw as Record<string, unknown>)?.["id"] ?? 0), | ||
| username: String((raw as Record<string, unknown>)?.["username"] ?? ""), | ||
| role: | ||
| ((raw as Record<string, unknown>)?.["role"] as "user" | "admin") || | ||
| "user", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could just cast once so you don't have to repeat yourself: const raw = unwrapData<unknown>(res.data) as Record<string, unknown>
Also, the way props of raw are accessed could be simpler (or better) in this case. e.g. raw?.id ,raw?.username instead of raw?.["id"], etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simplified in bf30ad4. Was like that because I've had some troubles with auth and was experimenting. Then I cleaned up a lot of garbage code, but forgot about this
|
|
||
| const refresh = useCallback(async () => { | ||
| try { | ||
| const res = await http.get<unknown>("/auth"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use Zod or Yup for data validation.
| const refresh = useCallback(async () => { | ||
| try { | ||
| const res = await http.get<unknown>("/auth"); | ||
| const raw = unwrapData<unknown>(res.data); | ||
| const normalized: User = { | ||
| id: Number((raw as Record<string, unknown>)?.["id"] ?? 0), | ||
| username: String((raw as Record<string, unknown>)?.["username"] ?? ""), | ||
| role: | ||
| ((raw as Record<string, unknown>)?.["role"] as "user" | "admin") || | ||
| "user", | ||
| }; | ||
| setUser(normalized); | ||
| } catch { | ||
| setUser(null); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, []); | ||
|
|
||
| const didInit = useRef(false); | ||
| useEffect(() => { | ||
| if (didInit.current) return; | ||
| didInit.current = true; | ||
| void refresh(); | ||
| }, [refresh]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though refresh is wrapped in useCallback with empty dependencies [], it's still technically recreated on every render! What you can do: move the refresh logic into the useEffect and get rid of the init ref check, making the effect run only on mount, It will look like this:
useEffect(() => {
const initAuth = async () => {
try {
const res = await http.get<unknown>("/auth");
// ... rest of refresh logic
} catch {
setUser(null);
} finally {
setLoading(false);
}
};
void initAuth();
}, []); // No dependencies, runs only on mount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in 60e4d72
| if (forbidden) { | ||
| emitNotification({ | ||
| type: "error", | ||
| message: "Нет прав для просмотра сниппета", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in fc5611d
| saveEdit: async () => { | ||
| try { | ||
| await updateQuestionMut.mutateAsync({ | ||
| title: editTitle, | ||
| description: editDescription, | ||
| attachedCode: editQCode, | ||
| }); | ||
| setIsEditing(false); | ||
| } catch { | ||
| emitNotification({ | ||
| type: "error", | ||
| message: "Не удалось сохранить изменения", | ||
| }); | ||
| } | ||
| }, | ||
| saving: updateQuestionMut.isPending, | ||
| deleteItem: async () => { | ||
| if (!confirm("Удалить вопрос?")) return; | ||
| try { | ||
| await deleteQuestionMut.mutateAsync(); | ||
| navigate("/my?mode=questions"); | ||
| } catch { | ||
| emitNotification({ | ||
| type: "error", | ||
| message: "Не удалось удалить вопрос", | ||
| }); | ||
| } | ||
| }, | ||
| deleting: deleteQuestionMut.isPending, | ||
| question: mappedQuestion, | ||
| edit: { | ||
| title: editTitle, | ||
| description: editDescription, | ||
| code: editQCode, | ||
| setTitle: setEditTitle, | ||
| setDescription: setEditDescription, | ||
| setCode: setEditQCode, | ||
| }, | ||
| answerForm, | ||
| markPending: setAnswerStateMut.isPending, | ||
| markCorrect: (answerId: string | number) => { | ||
| if (!isOwner) return; | ||
| setAnswerStateMut.mutate({ answerId, state: "correct" }); | ||
| }, | ||
| markIncorrect: (answerId: string | number) => { | ||
| if (!isOwner) return; | ||
| setAnswerStateMut.mutate({ answerId, state: "incorrect" }); | ||
| }, | ||
| updateAnswer: (answerId: string | number, content: string) => | ||
| updateAnswerMut.mutate( | ||
| { answerId, content }, | ||
| { | ||
| onSuccess: () => | ||
| emitAnswerUpdate({ | ||
| questionId: id || "0", | ||
| answerId, | ||
| content, | ||
| }), | ||
| } | ||
| ), | ||
| deleteAnswer: (answerId: string | number) => | ||
| deleteAnswerMut.mutate(answerId, { | ||
| onSuccess: () => emitAnswerDelete({ questionId: id || "0", answerId }), | ||
| }), | ||
| } as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to define callbacks outside the return block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in 68006dd
| saveEdit: async () => { | ||
| try { | ||
| await updateSnippetMut.mutateAsync({ | ||
| language: editLanguage, | ||
| code: editCode, | ||
| }); | ||
| setIsEditing(false); | ||
| } catch { | ||
| emitNotification({ | ||
| type: "error", | ||
| message: "Не удалось сохранить изменения", | ||
| }); | ||
| } | ||
| }, | ||
| saving: updateSnippetMut.isPending, | ||
| deleteItem: async () => { | ||
| if (!confirm("Удалить сниппет?")) return; | ||
| try { | ||
| await deleteSnippetMut.mutateAsync(); | ||
| navigate("/my?mode=snippets"); | ||
| } catch { | ||
| emitNotification({ | ||
| type: "error", | ||
| message: "Не удалось удалить сниппет", | ||
| }); | ||
| } | ||
| }, | ||
| deleting: deleteSnippetMut.isPending, | ||
| snippet: mappedSnippet, | ||
| edit: { | ||
| language: editLanguage, | ||
| code: editCode, | ||
| setLanguage: setEditLanguage, | ||
| setCode: setEditCode, | ||
| }, | ||
| commentForm, | ||
| updateComment: (id: number | string, content: string) => | ||
| updateCommentMut.mutate( | ||
| { id: Number(id), content }, | ||
| { | ||
| onSuccess: () => | ||
| emitSnippetCommentUpdate({ | ||
| snippetId: numericId || 0, | ||
| id, | ||
| content, | ||
| }), | ||
| } | ||
| ), | ||
| deleteComment: (id: number | string) => | ||
| deleteCommentMut.mutate(Number(id), { | ||
| onSuccess: () => | ||
| emitSnippetCommentDelete({ snippetId: numericId || 0, id }), | ||
| }), | ||
| } as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in 68006dd
| const textarea = ( | ||
| <textarea | ||
| rows={rows} | ||
| placeholder={placeholder} | ||
| className="w-full border rounded p-2 bg-white text-black dark:bg-neutral-800 dark:text-white" | ||
| {...("value" in props | ||
| ? { | ||
| value: props.value, | ||
| onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => | ||
| props.onChange && props.onChange(e.target.value), | ||
| } | ||
| : props.textareaProps)} | ||
| /> | ||
| ); | ||
|
|
||
| const body = ( | ||
| <> | ||
| {header} | ||
| {textarea} | ||
| <div className="flex items-center gap-2"> | ||
| <button | ||
| type={props.formSubmit ? "submit" : "button"} | ||
| onClick={props.formSubmit ? undefined : props.onSubmit} | ||
| disabled={buttonDisabled} | ||
| className="px-3 py-1.5 border rounded disabled:opacity-50 bg-white dark:bg-neutral-800"> | ||
| {pending ? "..." : submitLabel} | ||
| </button> | ||
| {error && <span className="text-red-500 text-sm">{error}</span>} | ||
| {success && <span className="text-green-600 text-sm">{success}</span>} | ||
| </div> | ||
| </> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we talked about this before. You shouldn't define components inside components even if it's a block of JSX. It's better to define them outside as standalone components that accepts props.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed in b7dd6d8. Could be in separate file, but I don't think that it's really necessary cuz it'll bloat UI folder more.
| useEffect(() => { | ||
| if (me?.username) setUsername(me.username); | ||
| }, [me?.username]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redundant code, you already initialize using useState
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was fixed in bb72864
No description provided.