Skip to content

Conversation

@elvisduru
Copy link
Collaborator

No description provided.

kotru21 added 30 commits August 13, 2025 16:19
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.
kotru21 and others added 11 commits August 24, 2025 23:49
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.
@vercel
Copy link

vercel bot commented Sep 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stackoverflow-clone Ready Ready Preview Comment Sep 8, 2025 11:34am

Comment on lines +8 to +9
const [atTop, setAtTop] = useState(true);
const atTopRef = useRef<boolean>(true);
Copy link
Collaborator Author

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.

Copy link
Owner

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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in f05daba

Comment on lines +70 to +73
<AuthContext.Provider
value={{ user, loading, login, logout, register, refresh }}>
{children}
</AuthContext.Provider>
Copy link
Collaborator Author

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.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bc0b028

Comment on lines +14 to +20
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",
Copy link
Collaborator Author

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.

Copy link
Owner

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");
Copy link
Collaborator Author

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.

Comment on lines +11 to +35
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]);
Copy link
Collaborator Author

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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 60e4d72

Comment on lines +49 to +54
if (forbidden) {
emitNotification({
type: "error",
message: "Нет прав для просмотра сниппета",
});
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in fc5611d

Comment on lines +81 to +145
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;
Copy link
Collaborator Author

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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 68006dd

Comment on lines +86 to +139
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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 68006dd

Comment on lines +60 to +91
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>
</>
);
Copy link
Collaborator Author

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.

Copy link
Owner

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.

Comment on lines +16 to +18
useEffect(() => {
if (me?.username) setUsername(me.username);
}, [me?.username]);
Copy link
Collaborator Author

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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was fixed in bb72864

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants