Skip to content
Merged

Dev #188

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6e0d5ce
standardize the tags casing (#157)
Tech-lo Mar 30, 2026
7e43129
update (#158)
Tech-lo Apr 3, 2026
ce6ce61
Merge branch 'dev' of https://github.com/OpenPecha/plan_frontend into…
Tech-lo Apr 21, 2026
8818e2b
fix(ui): improve responsiveness in profile edit form and user card la…
harshal-2304 Apr 28, 2026
f1efc67
feat(dashboard): show empty state with create plan action; clean up t…
harshal-2304 Apr 28, 2026
043a2a1
feat(pagination): add mobile-friendly icons for previous and next but…
harshal-2304 Apr 28, 2026
b977d3e
fix(create-plan): improve responsive layout and spacing for form sect…
harshal-2304 Apr 28, 2026
e6b873e
feat(image-upload): improve file selection UX and responsive layout; …
harshal-2304 Apr 28, 2026
bbff659
Remove unused import
harshal-2304 Apr 28, 2026
b8511ad
Format changes
harshal-2304 Apr 28, 2026
a859008
fix(image-upload): improve preview layout and apply proper filename t…
harshal-2304 Apr 29, 2026
1878141
feat(auth): add user avatar in navbar with profile navigation and ref…
harshal-2304 Apr 29, 2026
bd3c160
add plan start date (#168)
Tech-lo Apr 29, 2026
550f206
feat(avatar): introduce reusable avatar component and integrate into …
harshal-2304 Apr 29, 2026
a53c7f6
Sidebar bug fix (#171)
Tech-lo Apr 30, 2026
0fbd95b
Merge branch 'dev' of github-dharmaduta:OpenPecha/WeBuddhist-Studio i…
harshal-2304 Apr 30, 2026
01562d4
Solved format issue
harshal-2304 Apr 30, 2026
140066c
Merge pull request #167 from OpenPecha/fix/frontend-responsiveness-166
harshal-2304 Apr 30, 2026
c14bced
dark theme (#172)
Tech-lo Apr 30, 2026
47f2a09
feat(profile): improve form dirty state handling, image upload behavi…
harshal-2304 May 1, 2026
c77842b
update modal design
TenzDelek May 2, 2026
3466f52
lint
TenzDelek May 2, 2026
65ee1fb
Merge branch 'dev' of github-dharmaduta:OpenPecha/WeBuddhist-Studio i…
harshal-2304 May 4, 2026
edf66de
Merge pull request #176 from OpenPecha/fix/profile-save-behavior
harshal-2304 May 4, 2026
51c8ce9
sort fix
Tech-lo May 5, 2026
fe1d9d5
Revert "sort fix"
Tech-lo May 5, 2026
1d0fcee
feat(image-upload): add loading state, improve upload flow, and enhan…
harshal-2304 May 5, 2026
b51362f
fix(image-upload): prevent duplicate uploads using ref lock and refin…
harshal-2304 May 5, 2026
0dd4ee5
lint
harshal-2304 May 5, 2026
eed5918
feat(create-plan): enhance cover image section with detailed guidelin…
harshal-2304 May 6, 2026
5b60bea
feat(create-plan): introduce responsive tooltip for cover image guide…
harshal-2304 May 6, 2026
05e0004
refactor(create-plan): unify cover image constraint copy
harshal-2304 May 6, 2026
a8500ca
chore(tooling): add Cursor generate-commit-message command
harshal-2304 May 6, 2026
b8f34fe
fix(image-upload): reset modal state after successful upload
harshal-2304 May 7, 2026
3290b9e
feat(image-upload): prevent duplicate submits and add uploading state
harshal-2304 May 7, 2026
e529c16
refactor(image-upload): extract shared image upload draft hook
harshal-2304 May 8, 2026
87a8874
chore(.gitignore): update ignore patterns and remove unused command file
harshal-2304 May 8, 2026
4237745
refactor(image-upload): replace disabled state with uploadUiBusy
harshal-2304 May 8, 2026
bcd5b9d
Merge pull request #183 from OpenPecha/fix/prevent-multiple-image-upload
harshal-2304 May 8, 2026
e7b29d4
refactor(routes): standardize route paths and improve navigation
tenkus47 May 19, 2026
9566bb4
lint
TenzDelek May 20, 2026
799a550
Merge pull request #186 from OpenPecha/update_params
tenkus47 May 20, 2026
87e2383
feat(tags): add Tags component and integrate into routing
tenkus47 May 21, 2026
491717c
lint
Tech-lo May 21, 2026
f1f4b88
Merge pull request #187 from OpenPecha/new_tag_integration
tenkus47 May 21, 2026
cad57eb
Merge branch 'main' into dev
tenkus47 May 21, 2026
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ dist-ssr
*.sw?
.env
.env.production
coverage
coverage

.cursor
44 changes: 37 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@tanstack/react-query-devtools": "^5.100.6",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
Expand Down
12 changes: 3 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,15 @@ import { ACCESS_TOKEN, LANGUAGE, REFRESH_TOKEN } from "./lib/constant";
import { useAuth } from "./config/auth-context";
import { useMutation } from "@tanstack/react-query";
import axiosInstance from "./config/axios-config";
import { AUTH_ROUTE_PATHS, ROUTES } from "./routes/paths";

function App() {
const location = useLocation();
const { login } = useAuth();
const navigate = useNavigate();
const [intervalId, setIntervalId] = useState(null);

const authRoutes = [
"/login",
"/signup",
"/forgot-password",
"/reset-password",
"/verify-email",
];
const hideNavbar = authRoutes.includes(location.pathname);
const hideNavbar = AUTH_ROUTE_PATHS.includes(location.pathname);
const loginMutation = useMutation({
mutationFn: async (refreshToken: string) => {
const { data } = await axiosInstance.post(
Expand All @@ -41,7 +35,7 @@ function App() {
onError: () => {
sessionStorage.removeItem(ACCESS_TOKEN);
localStorage.removeItem(REFRESH_TOKEN);
navigate("/login");
navigate(ROUTES.login);
},
});

Expand Down
144 changes: 117 additions & 27 deletions src/components/routes/create-plan/CreatePlan.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("CreatePlan Component", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useParams).mockReturnValue({});
vi.spyOn(axiosInstance, "post").mockImplementation((url) => {
vi.spyOn(axiosInstance, "post").mockImplementation((url: string) => {
if (url.includes("/media/upload")) {
return Promise.resolve({
data: {
Expand All @@ -81,21 +81,70 @@ describe("CreatePlan Component", () => {
},
});
}
if (url.includes("/cms/tags")) {
return Promise.resolve({
data: {
id: "tag-new",
name: "Newtag",
image: null,
image_key: null,
description: null,
plan_ids: [],
},
});
}
return Promise.resolve({ data: {} });
});
vi.spyOn(axiosInstance, "get").mockResolvedValue({
data: {
id: "plan-123",
title: "Existing Plan",
description: "Existing description",
total_days: 14,
difficulty_level: "Beginner",
plan_image_url: "https://example.com/image.jpg",
image_url: "https://example.com/image.jpg",
tags: ["meditation"],
language: "en",
start_date: "2026-04-30T00:00:00Z",
},
vi.spyOn(axiosInstance, "get").mockImplementation((url: string) => {
if (url.includes("/cms/tags")) {
return Promise.resolve({
data: {
tags: [
{
id: "tag-1",
name: "Meditation",
image: null,
image_key: null,
description: null,
plan_ids: [],
},
{
id: "tag-2",
name: "Daily tipitaka",
image: null,
image_key: null,
description: null,
plan_ids: [],
},
],
skip: 0,
limit: 500,
total: 2,
},
});
}
return Promise.resolve({
data: {
id: "plan-123",
title: "Existing Plan",
description: "Existing description",
total_days: 14,
difficulty_level: "Beginner",
plan_image_url: "https://example.com/image.jpg",
image_url: "https://example.com/image.jpg",
tags: [
{
id: "tag-1",
name: "Meditation",
image: null,
image_key: null,
description: null,
},
],
language: "en",
start_date: "2026-04-30T00:00:00Z",
},
});
});
});
it("renders create plan form with main heading", () => {
Expand Down Expand Up @@ -147,7 +196,7 @@ describe("CreatePlan Component", () => {
screen.getByText("studio.dashboard.cover_image"),
).toBeInTheDocument();
expect(
screen.getByText("studio.plan.cover_image.description"),
screen.getByText("studio.plan.cover_image.constraints"),
).toBeInTheDocument();
});

Expand All @@ -162,7 +211,7 @@ describe("CreatePlan Component", () => {
it("renders submit button", () => {
renderWithProviders(<CreatePlan />);

const submitButton = screen.getByText("studio.plan.update_button");
const submitButton = screen.getByText("studio.plan.next_button");
expect(submitButton).toBeInTheDocument();
expect(submitButton.tagName).toBe("BUTTON");
});
Expand Down Expand Up @@ -211,7 +260,7 @@ describe("CreatePlan Component", () => {
screen.getByText("studio.dashboard.cover_image"),
).toBeInTheDocument();
expect(
screen.getByText("studio.plan.cover_image.description"),
screen.getByText("studio.plan.cover_image.constraints"),
).toBeInTheDocument();
const uploadButton = screen.getByLabelText("Upload cover image");
expect(uploadButton).toBeInTheDocument();
Expand Down Expand Up @@ -301,7 +350,7 @@ describe("CreatePlan Component", () => {
});

it("shows validation errors for required fields", async () => {
vi.mocked(useParams).mockReturnValue({ plan_id: "new" });
vi.mocked(useParams).mockReturnValue({});
renderWithProviders(<CreatePlan />);
const submitButton = screen.getByText("studio.plan.next_button");
fireEvent.click(submitButton);
Expand All @@ -316,12 +365,53 @@ describe("CreatePlan Component", () => {
).toBeInTheDocument();
});

it("handles tag input add and remove", () => {
it("handles tag search selection and remove", async () => {
renderWithProviders(<CreatePlan />);
const tagInput = screen.getByPlaceholderText("Add a tag");
fireEvent.change(tagInput, { target: { value: "tag1" } });
fireEvent.keyDown(tagInput, { key: "Enter", code: "Enter" });
expect(screen.getByText("Tag1")).toBeInTheDocument();
const tagInput = screen.getByPlaceholderText("Search or add tags...");
fireEvent.change(tagInput, { target: { value: "Med" } });
fireEvent.focus(tagInput);

await waitFor(() => {
expect(
screen.getByRole("button", { name: "Meditation" }),
).toBeInTheDocument();
});

fireEvent.click(screen.getByRole("button", { name: "Meditation" }));
expect(screen.getAllByText("Meditation").length).toBeGreaterThan(0);

const removeButton = screen.getByRole("button", {
name: /Remove Meditation/i,
});
fireEvent.click(removeButton);
expect(
screen.queryByRole("button", { name: /Remove Meditation/i }),
).toBeNull();
});

it("creates a new tag when typing a name that does not exist", async () => {
renderWithProviders(<CreatePlan />);
const tagInput = screen.getByPlaceholderText("Search or add tags...");
fireEvent.change(tagInput, { target: { value: "Brand New Tag" } });
fireEvent.focus(tagInput);

await waitFor(() => {
expect(
screen.getByRole("button", { name: 'Create "Brand New Tag"' }),
).toBeInTheDocument();
});

fireEvent.click(
screen.getByRole("button", { name: 'Create "Brand New Tag"' }),
);

await waitFor(() => {
expect(axiosInstance.post).toHaveBeenCalledWith(
"/api/v1/cms/tags",
expect.objectContaining({ name: "Brand New Tag" }),
expect.any(Object),
);
});
});

it("shows no image preview initially", () => {
Expand Down Expand Up @@ -350,7 +440,7 @@ describe("CreatePlan Component", () => {
fireEvent.click(difficultyButton);
const difficultyOption = screen.getByText("Beginner");
fireEvent.click(difficultyOption);
const submitButton = screen.getByText("studio.plan.update_button");
const submitButton = screen.getByText("studio.plan.next_button");
fireEvent.click(submitButton);
await waitFor(() => {
expect(titleInput).toHaveValue("Test Plan");
Expand Down Expand Up @@ -396,7 +486,7 @@ describe("CreatePlan Component", () => {
});

it("fetches and populates form in edit mode", async () => {
vi.mocked(useParams).mockReturnValue({ plan_id: "plan-123" });
vi.mocked(useParams).mockReturnValue({ planId: "plan-123" });

renderWithProviders(<CreatePlan />);

Expand Down Expand Up @@ -442,7 +532,7 @@ describe("CreatePlan Component", () => {
});

it("submits update in edit mode", async () => {
vi.mocked(useParams).mockReturnValue({ plan_id: "plan-123" });
vi.mocked(useParams).mockReturnValue({ planId: "plan-123" });
vi.spyOn(axiosInstance, "put").mockResolvedValue({ data: {} });
renderWithProviders(<CreatePlan />);

Expand Down Expand Up @@ -472,7 +562,7 @@ describe("CreatePlan Component", () => {
});

it("clears start_date when switching from specific to enroll mode on submit", async () => {
vi.mocked(useParams).mockReturnValue({ plan_id: "plan-123" });
vi.mocked(useParams).mockReturnValue({ planId: "plan-123" });
const putSpy = vi
.spyOn(axiosInstance, "put")
.mockResolvedValue({ data: {} });
Expand Down
Loading
Loading