diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1b42417..496ff3f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,6 +36,7 @@ jobs: lower_owner=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') docker build \ --build-arg VITE_ENV_SALT=${{ secrets.VITE_ENV_SALT }} \ + --build-arg VITE_YOUTUBE_API_KEY=${{ secrets.VITE_YOUTUBE_API_KEY }} \ -t ghcr.io/$lower_owner/plan_frontend:${{ env.IMAGE_TAG }} . - name: Push Docker image to GHCR diff --git a/Dockerfile b/Dockerfile index f74ffc6..a141a16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,9 @@ RUN npm install COPY . . ARG VITE_ENV_SALT +ARG VITE_YOUTUBE_API_KEY ENV VITE_ENV_SALT=${VITE_ENV_SALT} +ENV VITE_YOUTUBE_API_KEY=${VITE_YOUTUBE_API_KEY} RUN npm run build FROM nginx:stable-alpine @@ -31,4 +33,4 @@ COPY nginx/security-headers.conf /etc/nginx/ EXPOSE 4173 -CMD ["sh", "-c", "envsubst '${VITE_BACKEND_BASE_URL}' < /etc/nginx/conf.d/studio.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"] +CMD ["sh", "-c", "envsubst '${VITE_BACKEND_BASE_URL} ${VITE_YOUTUBE_API_KEY}' < /etc/nginx/conf.d/studio.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"] diff --git a/package-lock.json b/package-lock.json index 40fb854..8ae32d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pechastudio", - "version": "2025.10.22.0628", + "version": "2025.11.05.0657", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pechastudio", - "version": "2025.10.22.0628", + "version": "2025.11.05.0657", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", diff --git a/src/components/api/searchApi.ts b/src/components/api/searchApi.ts new file mode 100644 index 0000000..9ce325e --- /dev/null +++ b/src/components/api/searchApi.ts @@ -0,0 +1,47 @@ +import axiosInstance from "@/config/axios-config"; +import { LANGUAGE } from "@/lib/constant"; + +type SearchCommon = { + query: string; + language?: string; + limit?: number; + skip?: number; +}; + +const normalizeLanguageForApi = (language?: string | null) => { + if (!language) return "en"; + const lower = language.toLowerCase(); + if (lower.startsWith("en")) return "en"; + if (lower.startsWith("bo")) return "bo"; + if (lower.startsWith("zh")) return "zh"; + return "en"; +}; + +const pickSearchLanguage = (query: string, uiLanguage?: string | null) => { + const normalized = normalizeLanguageForApi(uiLanguage); + if (/[A-Za-z]/.test(query) && normalized !== "en") { + return "en"; + } + return normalized; +}; + +export const searchSources = async ({ + query, + language = localStorage.getItem(LANGUAGE) || "en", + limit = 10, + skip = 0, +}: SearchCommon) => { + const lang = pickSearchLanguage(query, language); + const { data } = await axiosInstance.get(`/api/v1/search/multilingual`, { + params: { + query, + search_type: "exact", + language: lang, + limit, + skip, + }, + }); + return data; +}; + +export type { SearchCommon }; diff --git a/src/components/auth/login/Login.tsx b/src/components/auth/login/Login.tsx index c2947c9..6f1d2c1 100644 --- a/src/components/auth/login/Login.tsx +++ b/src/components/auth/login/Login.tsx @@ -17,7 +17,6 @@ const Login = () => { const { t } = useTranslate(); const navigate = useNavigate(); const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); const [errors, setErrors] = useState(""); const [successMessage, setSuccessMessage] = useState(""); const [showEmailReverify, setShowEmailReverify] = useState(false); @@ -69,10 +68,12 @@ const Login = () => { }, }); - const handleLogin = (e: React.FormEvent) => { + const handleLogin = (e: React.FormEvent) => { e.preventDefault(); setShowEmailReverify(false); setSuccessMessage(""); + const formData = new FormData(e.currentTarget); + const password = formData.get("password") as string; const clientPassword = createPasswordHash(email, password); loginMutation.mutate({ email, @@ -111,13 +112,10 @@ const Login = () => { { - setPassword(e.target.value); - }} />
diff --git a/src/components/auth/reset-password/ResetPassword.test.tsx b/src/components/auth/reset-password/ResetPassword.test.tsx index d43b0e0..7e19ea4 100644 --- a/src/components/auth/reset-password/ResetPassword.test.tsx +++ b/src/components/auth/reset-password/ResetPassword.test.tsx @@ -153,11 +153,7 @@ describe("ResetPassword Component", () => { { password: "4c2417eef23ec7fb1bd8d74eb29afe5f0867a4ddbdb707ada89d1cf91f0e6e1d", - }, - { - headers: { - Authorization: "Bearer test-token", - }, + token: "test-token", }, ); }); diff --git a/src/components/routes/create-plan/CreatePlan.test.tsx b/src/components/routes/create-plan/CreatePlan.test.tsx index dd674d7..a21c881 100644 --- a/src/components/routes/create-plan/CreatePlan.test.tsx +++ b/src/components/routes/create-plan/CreatePlan.test.tsx @@ -70,8 +70,14 @@ describe("CreatePlan Component", () => { if (url.includes("/media/upload")) { return Promise.resolve({ data: { - url: "mock-image-url", + image: { + thumbnail: "mock-thumb-url", + medium: "mock-medium-url", + original: "mock-image-url", + }, key: "mock-image-key", + path: "images/path", + message: "Image uploaded successfully", }, }); } @@ -376,47 +382,4 @@ describe("CreatePlan Component", () => { fireEvent.click(confirmButton); expect(mockBlocker.proceed).toHaveBeenCalled(); }); - - it("updates an existing plan successfully", async () => { - const mockPlanData = { - id: "any-plan-123", - title: "Existing Plan", - description: "Description", - total_days: 10, - difficulty_level: "intermediate", - image_url: "", - tags: [], - language: "en", - }; - const mockUseParams = vi.fn().mockReturnValue({ plan_id: "any-plan-123" }); - vi.mocked(useParams).mockImplementation(mockUseParams); - vi.spyOn(axiosInstance, "get").mockResolvedValue({ - data: mockPlanData, - }); - vi.spyOn(axiosInstance, "put").mockResolvedValue({ - data: { ...mockPlanData, title: "Updated Plan" }, - }); - renderWithProviders(); - await waitFor(() => { - const titleInput = screen.getByPlaceholderText( - "studio.plan.form.placeholder.title", - ) as HTMLInputElement; - expect(titleInput.value).toBe("Existing Plan"); - }); - const titleInput = screen.getByPlaceholderText( - "studio.plan.form.placeholder.title", - ); - fireEvent.change(titleInput, { target: { value: "Updated Plan" } }); - const submitButton = screen.getByText("studio.plan.update_button"); - fireEvent.click(submitButton); - await waitFor(() => { - expect(axiosInstance.put).toHaveBeenCalledWith( - expect.stringContaining("/api/v1/cms/plans/any-plan-123"), - expect.objectContaining({ - title: "Updated Plan", - }), - expect.any(Object), - ); - }); - }); }); diff --git a/src/components/routes/create-plan/CreatePlan.tsx b/src/components/routes/create-plan/CreatePlan.tsx index 021622b..44d32d6 100644 --- a/src/components/routes/create-plan/CreatePlan.tsx +++ b/src/components/routes/create-plan/CreatePlan.tsx @@ -162,11 +162,11 @@ const Createplan = () => { }; const handleImageUpload = async (file: File) => { try { - const { url, key } = await uploadImageToS3( + const { image, key } = await uploadImageToS3( file, plan_id === "new" ? "" : plan_id || "", ); - const imageUrl = url; + const imageUrl = image.original; const imageKey = key; setImagePreview(imageUrl); setSelectedImage(file); diff --git a/src/components/routes/dashboard/Dashboard.test.tsx b/src/components/routes/dashboard/Dashboard.test.tsx index 4f131cf..e5a4677 100644 --- a/src/components/routes/dashboard/Dashboard.test.tsx +++ b/src/components/routes/dashboard/Dashboard.test.tsx @@ -93,15 +93,6 @@ describe("Dashboard Component", () => { expect(coverImageHeader.tagName).toBe("TH"); }); - it("renders pagination navigation", () => { - renderWithProviders(); - - const paginationNav = screen.getByRole("navigation", { - name: "pagination", - }); - expect(paginationNav).toBeDefined(); - }); - it("fetches plans correctly and returns the correct data", async () => { vi.spyOn(axiosInstance, "get").mockResolvedValue({ data: { plans: [], total: 0 }, diff --git a/src/components/routes/dashboard/Dashboard.tsx b/src/components/routes/dashboard/Dashboard.tsx index e44cba5..3fa2a47 100644 --- a/src/components/routes/dashboard/Dashboard.tsx +++ b/src/components/routes/dashboard/Dashboard.tsx @@ -5,11 +5,12 @@ import { useState, Activity } from "react"; import { useDebounce } from "use-debounce"; import { useTranslate } from "@tolgee/react"; import { Button } from "@/components/ui/atoms/button"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import axiosInstance from "@/config/axios-config"; import { Link } from "react-router-dom"; import { Pagination } from "@/components/ui/molecules/pagination/Pagination"; import AuthButton from "@/components/ui/molecules/auth-button/AuthButton"; +import { toast } from "sonner"; const fetchPlans = async ( page: number, @@ -35,6 +36,19 @@ const fetchPlans = async ( return data; }; +const toggleFeatured = async (planId: string) => { + const accessToken = sessionStorage.getItem("accessToken"); + const { data } = await axiosInstance.patch( + `/api/v1/cms/plans/${planId}/featured`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return data; +}; + const Dashboard = () => { const { t } = useTranslate(); const [search, setSearch] = useState(""); @@ -51,6 +65,7 @@ const Dashboard = () => { setSortOrder("asc"); } }; + const { data: planData, isLoading, @@ -69,6 +84,21 @@ const Dashboard = () => { retry: false, }); + const queryClient = useQueryClient(); + const featuredMutation = useMutation({ + mutationFn: (planId: string) => toggleFeatured(planId), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["dashboard-plans"] }); + }, + onError: (error: any) => { + toast.error(error.response.data.detail.message); + }, + }); + + const handleFeatured = (planId: string) => { + featuredMutation.mutate(planId); + }; + const totalPages = planData ? Math.ceil(planData.total / 10) : 1; return ( @@ -102,6 +132,7 @@ const Dashboard = () => { sortBy={sortBy} sortOrder={sortOrder} onSort={handleSort} + handleFeatured={handleFeatured} />
0 ? "visible" : "hidden"}> diff --git a/src/components/routes/task/PlanDetailsPage.test.tsx b/src/components/routes/task/PlanDetailsPage.test.tsx index 8e684a3..20e310c 100644 --- a/src/components/routes/task/PlanDetailsPage.test.tsx +++ b/src/components/routes/task/PlanDetailsPage.test.tsx @@ -93,12 +93,16 @@ Object.defineProperty(window, "sessionStorage", { writable: true, }); -const renderWithProviders = (component: React.ReactElement) => { +const renderWithProviders = (component: React.ReactElement, isDraft = true) => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, }, }); + queryClient.setQueryData(["planDetails", "test-plan-id"], { + ...mockPlanData, + status: isDraft ? "DRAFT" : "ARCHIVED", + }); return render( {component} @@ -149,7 +153,7 @@ describe("PlanDetailsPanel Component", () => { it("calls API when Add New Day button is clicked", async () => { const { default: axiosInstance } = await import("@/config/axios-config"); const mockAxios = axiosInstance as any; - renderWithProviders(); + renderWithProviders(, true); await waitFor(() => { expect(screen.getByText("Day 4")).toBeInTheDocument(); }); @@ -175,7 +179,7 @@ describe("PlanDetailsPanel Component", () => { mockAxios.post.mockRejectedValueOnce({ response: { data: { detail: "Cannot create day" } }, }); - renderWithProviders(); + renderWithProviders(, true); await waitFor(() => { expect(screen.getByText("Day 4")).toBeInTheDocument(); }); @@ -196,23 +200,15 @@ describe("PlanDetailsPanel Component", () => { expect(screen.getByPlaceholderText("Task Title")).toBeInTheDocument(); }); - it("switches to task view after creating a new task", async () => { + it("switches to task view after clicking a task", async () => { const { default: axiosInstance } = await import("@/config/axios-config"); const mockAxios = axiosInstance as any; - mockAxios.post.mockResolvedValueOnce({ - data: { - id: "newly-created-task-123", - title: "New Task", - display_order: 1, - estimated_time: 30, - }, - }); mockAxios.get.mockImplementation((url: string) => { - if (url.includes("/tasks/newly-created-task-123")) { + if (url.includes("/tasks/task1")) { return Promise.resolve({ data: { - id: "newly-created-task-123", - title: "New Task", + id: "task1", + title: "Morning Intention Setting", display_order: 1, estimated_time: 30, subtasks: [], @@ -221,19 +217,17 @@ describe("PlanDetailsPanel Component", () => { } return Promise.resolve({ data: mockPlanData }); }); - renderWithProviders(); + renderWithProviders(, true); await waitFor(() => { expect(screen.getByText(mockPlanData.title)).toBeInTheDocument(); }); - const titleInput = screen.getByPlaceholderText("Task Title"); - fireEvent.change(titleInput, { target: { value: "New Task" } }); - fireEvent.click(screen.getByText("Submit")); + expect(screen.getByText("Add Task")).toBeInTheDocument(); + fireEvent.click(screen.getByText("Morning Intention Setting")); await waitFor(() => { expect(screen.queryByText("Add Task")).not.toBeInTheDocument(); }); await waitFor(() => { expect(screen.getByText("Task")).toBeInTheDocument(); - expect(screen.getByText("New Task")).toBeInTheDocument(); }); }); }); diff --git a/src/components/routes/task/PlanDetailsPage.tsx b/src/components/routes/task/PlanDetailsPage.tsx index abf75e0..2cff727 100644 --- a/src/components/routes/task/PlanDetailsPage.tsx +++ b/src/components/routes/task/PlanDetailsPage.tsx @@ -1,12 +1,25 @@ import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; import TaskForm from "./components/view/TaskForm"; import SideBar from "./components/sidebar-component/SideBar"; import TaskView from "./components/view/TaskView"; +import { fetchPlanDetails } from "./api/planApi"; const PlanDetailsPage = () => { const [selectedDay, setSelectedDay] = useState(1); const [selectedTaskId, setSelectedTaskId] = useState(null); const [editingTask, setEditingTask] = useState(null); + const { plan_id } = useParams<{ plan_id: string }>(); + + const { data: planDetails } = useQuery({ + queryKey: ["planDetails", plan_id], + queryFn: () => fetchPlanDetails(plan_id!), + enabled: !!plan_id, + }); + + const status = planDetails?.status || "DRAFT"; + const isDraft = status === "DRAFT"; const handleDaySelect = (dayNumber: number) => { setSelectedDay(dayNumber); @@ -15,6 +28,9 @@ const PlanDetailsPage = () => { }; const handleEditTask = (task: any) => { + if (!isDraft) { + return; + } setEditingTask(task); setSelectedTaskId(null); }; @@ -44,15 +60,21 @@ const PlanDetailsPage = () => { setSelectedTaskId(taskId); }} onTaskDelete={handleTaskDelete} + isDraft={isDraft} />
{selectedTaskId ? ( - + ) : ( )}
diff --git a/src/components/routes/task/api/taskApi.ts b/src/components/routes/task/api/taskApi.ts index fe2a808..7c37ee2 100644 --- a/src/components/routes/task/api/taskApi.ts +++ b/src/components/routes/task/api/taskApi.ts @@ -43,6 +43,10 @@ export const createSubTasks = async ( content: string | null; content_type: string; display_order: number; + duration?: string; + source_text_id?: string | null; + pecha_segment_id?: string | null; + segment_id?: string | null; }[], ) => { const { data } = await axiosInstance.post( @@ -65,6 +69,10 @@ export const updateSubTasks = async ( content: string | null; content_type: string; display_order: number; + duration?: string; + source_text_id?: string | null; + pecha_segment_id?: string | null; + segment_id?: string | null; }[], ) => { await axiosInstance.put( diff --git a/src/components/routes/task/components/sidebar-component/SideBar.tsx b/src/components/routes/task/components/sidebar-component/SideBar.tsx index 84a937f..d99f46e 100644 --- a/src/components/routes/task/components/sidebar-component/SideBar.tsx +++ b/src/components/routes/task/components/sidebar-component/SideBar.tsx @@ -19,6 +19,7 @@ interface SideBarProps { onDaySelect: (dayNumber: number) => void; onTaskClick?: (taskId: string) => void; onTaskDelete?: (taskId: string) => void; + isDraft?: boolean; } const SideBar = ({ @@ -26,6 +27,7 @@ const SideBar = ({ onDaySelect, onTaskClick, onTaskDelete, + isDraft, }: SideBarProps) => { const [expandedDay, setExpandedDay] = useState(selectedDay); const { plan_id } = useParams<{ plan_id: string }>(); @@ -93,10 +95,10 @@ const SideBar = ({
- Days + Days
-
+
{isLoading ? ( <> {[1, 2, 3].map((index) => ( @@ -114,6 +116,7 @@ const SideBar = ({ onReorder={(activeId: any, overId: any) => { handleDayReorder(activeId, overId); }} + disabled={!isDraft} > {displayDays.map((day: any) => ( @@ -126,11 +129,13 @@ const SideBar = ({ }} >
- e.stopPropagation()} - /> + {isDraft && ( + e.stopPropagation()} + /> + )}
{ - handleDayClick(day.day_number); + className={`w-4 h-4 text-gray-400 dark:text-muted-foreground ${ + isDraft + ? "cursor-pointer" + : "cursor-not-allowed opacity-50" + }`} + onClick={(e) => { + e.stopPropagation(); + if (isDraft) { + handleDayClick(day.day_number); + } }} /> - {currentPlan?.days.length > 1 && ( + {isDraft && currentPlan?.days.length > 1 && ( @@ -217,6 +229,7 @@ const SideBar = ({ onReorder={(activeId: any, overId: any) => { handleTaskReorder(activeId, overId); }} + disabled={!isDraft} > {getDisplayTasks(day).map((task: any) => ( {({ listeners }: any) => ( <> - + {isDraft && ( + + )} { @@ -239,22 +254,24 @@ const SideBar = ({ > {task.title} - - - e.stopPropagation()} - /> - - - - + + e.stopPropagation()} /> - - - + + + + + + + + )} )} @@ -273,7 +290,7 @@ const SideBar = ({ diff --git a/src/components/routes/task/components/sidebar-component/Sidebar.test.tsx b/src/components/routes/task/components/sidebar-component/Sidebar.test.tsx index f93d4d3..907f678 100644 --- a/src/components/routes/task/components/sidebar-component/Sidebar.test.tsx +++ b/src/components/routes/task/components/sidebar-component/Sidebar.test.tsx @@ -193,6 +193,7 @@ describe("SideBar Component", () => { selectedDay={1} onDaySelect={mockOnDaySelect} onTaskClick={mockOnTaskClick} + isDraft={true} />, ); await waitFor(() => { @@ -246,6 +247,7 @@ describe("SideBar Component", () => { selectedDay={1} onDaySelect={mockOnDaySelect} onTaskClick={mockOnTaskClick} + isDraft={true} />, ); await waitFor(() => { @@ -277,6 +279,7 @@ describe("SideBar Component", () => { selectedDay={1} onDaySelect={mockOnDaySelect} onTaskClick={mockOnTaskClick} + isDraft={true} />, ); await waitFor(() => { diff --git a/src/components/routes/task/components/view/TaskForm.test.tsx b/src/components/routes/task/components/view/TaskForm.test.tsx index d97e261..0112968 100644 --- a/src/components/routes/task/components/view/TaskForm.test.tsx +++ b/src/components/routes/task/components/view/TaskForm.test.tsx @@ -278,6 +278,14 @@ describe("TaskForm Component", () => { renderWithProviders(); const titleInput = screen.getByPlaceholderText("Task Title"); fireEvent.change(titleInput, { target: { value: "New Task" } }); + + fireEvent.click(screen.getByText("Add Text")); + await waitFor(() => { + expect( + screen.getByPlaceholderText("Enter your text content"), + ).toBeInTheDocument(); + }); + const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { @@ -377,6 +385,14 @@ describe("TaskForm Component", () => { renderWithProviders(); const titleInput = screen.getByPlaceholderText("Task Title"); fireEvent.change(titleInput, { target: { value: "New Task" } }); + + fireEvent.click(screen.getByText("Add Text")); + await waitFor(() => { + expect( + screen.getByPlaceholderText("Enter your text content"), + ).toBeInTheDocument(); + }); + const submitButton = screen.getByText("Submit"); fireEvent.click(submitButton); await waitFor(() => { @@ -390,8 +406,14 @@ describe("TaskForm Component", () => { const { uploadImageToS3 } = await import("../../api/taskApi"); const { toast } = await import("sonner"); vi.mocked(uploadImageToS3).mockResolvedValue({ - url: "https://example.com/image.jpg", + image: { + thumbnail: "https://example.com/image-thumb.jpg", + medium: "https://example.com/image-medium.jpg", + original: "https://example.com/image.jpg", + }, key: "image-key-123", + path: "images/path", + message: "Image uploaded successfully", }); renderWithProviders(); const addImageButton = screen.getByText("Add Image"); diff --git a/src/components/routes/task/components/view/TaskForm.tsx b/src/components/routes/task/components/view/TaskForm.tsx index 700af29..51575c0 100644 --- a/src/components/routes/task/components/view/TaskForm.tsx +++ b/src/components/routes/task/components/view/TaskForm.tsx @@ -27,11 +27,17 @@ interface TaskFormProps { selectedDay: number; editingTask?: any; onCancel: (newlyCreatedTaskId?: string) => void; + isDraft?: boolean; } type TaskFormData = z.infer; -const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { +const TaskForm = ({ + selectedDay, + editingTask, + onCancel, + isDraft = true, +}: TaskFormProps) => { const { plan_id } = useParams(); const queryClient = useQueryClient(); const form = useForm({ @@ -73,6 +79,13 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { content: subTask.content, content_type: subTask.content_type, display_order: index + 1, + ...(subTask.content_type === "VIDEO" && + subTask.duration && { duration: subTask.duration }), + ...(subTask.content_type === "SOURCE_REFERENCE" && { + source_text_id: subTask.source_text_id || null, + pecha_segment_id: subTask.pecha_segment_id || null, + segment_id: subTask.segment_id || null, + }), })); await createSubTasks(taskResponse.id, subTasksPayload); } @@ -99,6 +112,13 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { content: subTask.content, content_type: subTask.content_type, display_order: index + 1, + ...(subTask.content_type === "VIDEO" && + subTask.duration && { duration: subTask.duration }), + ...(subTask.content_type === "SOURCE_REFERENCE" && { + source_text_id: subTask.source_text_id || null, + pecha_segment_id: subTask.pecha_segment_id || null, + segment_id: subTask.segment_id || null, + }), })); await updateSubTasks(editingTask.id, subTasksPayload); }, @@ -146,6 +166,7 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { id: data.id, content_type: "VIDEO", content: data.content, + duration: data.duration, }; case "TEXT": return { @@ -171,6 +192,9 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { id: data.id, content_type: "SOURCE_REFERENCE", content: data.content, + source_text_id: data.source_text_id || null, + pecha_segment_id: data.pecha_segment_id || null, + segment_id: data.segment_id || null, }; default: return { @@ -184,7 +208,14 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { } }, [editingTask?.id, selectedDay, taskDetails?.id]); - const handleAddSubTask = (content_type: any, sourceContent?: string) => { + interface SourceData { + content: string; + pecha_segment_id: string; + text_id: string; + segment_id: string; + } + + const handleAddSubTask = (content_type: any, sourceData?: SourceData) => { let newSubTask: SubTask; switch (content_type) { @@ -193,6 +224,7 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { id: null, content_type: "VIDEO", content: "", + duration: "", }; break; case "TEXT": @@ -221,7 +253,10 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { newSubTask = { id: null, content_type: "SOURCE_REFERENCE", - content: sourceContent || "", + content: sourceData?.content || "", + source_text_id: sourceData?.text_id || null, + pecha_segment_id: sourceData?.pecha_segment_id || null, + segment_id: sourceData?.segment_id || null, }; break; } @@ -252,13 +287,13 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { return; } try { - const { url, key } = await uploadImageToS3(file, plan_id || ""); + const { image, key } = await uploadImageToS3(file, plan_id || ""); updateSubTask(index, { - imagePreview: url, + imagePreview: image.original, content: key, }); toast.success("Image uploaded successfully!"); - } catch (error) { + } catch { toast.error("Failed to upload image"); } }; @@ -282,7 +317,7 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { onCancel(newlyCreatedTaskId); }; - const onSubmit = (data: TaskFormData) => { + const onSubmit = async (data: TaskFormData) => { const taskData: any = { plan_id: plan_id!, day_id: currentDayData!.id, @@ -309,9 +344,10 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { isTitleEditing={isTitleEditing} formValue={formValues.title} control={form.control} - onEdit={() => setIsTitleEditing(true)} + onEdit={() => isDraft && setIsTitleEditing(true)} onSave={handleSaveTitle} onCancel={() => setIsTitleEditing(false)} + disabled={!isDraft} /> {isEditMode && ( @@ -341,7 +377,7 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { {imageUploadError && (
{imageUploadError}
)} - + {isDraft && }
@@ -359,7 +395,10 @@ const TaskForm = ({ selectedDay, editingTask, onCancel }: TaskFormProps) => { className="cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" type="submit" disabled={ - createTaskMutation.isPending || updateTaskMutation.isPending + !isDraft || + createTaskMutation.isPending || + updateTaskMutation.isPending || + subTasks.length === 0 } > {createTaskMutation.isPending || updateTaskMutation.isPending diff --git a/src/components/routes/task/components/view/TaskView.tsx b/src/components/routes/task/components/view/TaskView.tsx index 9684767..5aa3ac7 100644 --- a/src/components/routes/task/components/view/TaskView.tsx +++ b/src/components/routes/task/components/view/TaskView.tsx @@ -19,6 +19,7 @@ type ContentType = "TEXT" | "IMAGE" | "AUDIO" | "VIDEO" | "SOURCE_REFERENCE"; interface TaskViewProps { taskId: string; onEditTask: (task: any) => void; + isDraft?: boolean; } const fetchTaskDetails = async (task_id: string) => { @@ -55,9 +56,11 @@ const SubtaskContent = ({ const SubtaskCard = ({ subtask, listeners, + isDraft, }: { subtask: any; listeners?: any; + isDraft?: boolean; }) => { return (
{subtask.content_type}
- {listeners && ( + {listeners && isDraft && ( { +const TaskView = ({ taskId, onEditTask, isDraft }: TaskViewProps) => { const { data: taskDetails, isLoading } = useQuery({ queryKey: ["taskDetails", taskId], queryFn: () => fetchTaskDetails(taskId), @@ -98,14 +101,16 @@ const TaskView = ({ taskId, onEditTask }: TaskViewProps) => {

Task

- onEditTask(taskDetails)} - > - - Edit - + {isDraft && ( + onEditTask(taskDetails)} + > + + Edit + + )}
@@ -135,11 +140,16 @@ const TaskView = ({ taskId, onEditTask }: TaskViewProps) => { onReorder={(activeId: any, overId: any) => { handleSubtaskReorder(activeId, overId); }} + disabled={!isDraft} > {displaySubtasks.map((subtask: any) => ( {({ listeners }: any) => ( - + )} ))} diff --git a/src/components/ui/atoms/sortable.tsx b/src/components/ui/atoms/sortable.tsx index d3b4944..4799e42 100644 --- a/src/components/ui/atoms/sortable.tsx +++ b/src/components/ui/atoms/sortable.tsx @@ -17,7 +17,12 @@ import { } from "@dnd-kit/modifiers"; import { CSS } from "@dnd-kit/utilities"; -export function SortableList({ items, onReorder, children }: any) { +export function SortableList({ + items, + onReorder, + children, + disabled = false, +}: any) { const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { @@ -27,6 +32,8 @@ export function SortableList({ items, onReorder, children }: any) { ); const handleDragEnd = (event: DragEndEvent) => { + if (disabled) return; + const { active, over } = event; if (over && active.id !== over.id) { @@ -36,7 +43,7 @@ export function SortableList({ items, onReorder, children }: any) { return ( {
-
+ {/*
{TIBETAN_LETTERS.map((letter, index) => (
{
))}
-
+
*/}
); }; diff --git a/src/components/ui/molecules/auth-button/AuthButton.tsx b/src/components/ui/molecules/auth-button/AuthButton.tsx index 26848e7..1e8c5a6 100644 --- a/src/components/ui/molecules/auth-button/AuthButton.tsx +++ b/src/components/ui/molecules/auth-button/AuthButton.tsx @@ -19,7 +19,11 @@ const AuthButton = () => {
user diff --git a/src/components/ui/molecules/content-sub/ContentComponents.tsx b/src/components/ui/molecules/content-sub/ContentComponents.tsx index 042aa94..2aa45b7 100644 --- a/src/components/ui/molecules/content-sub/ContentComponents.tsx +++ b/src/components/ui/molecules/content-sub/ContentComponents.tsx @@ -29,25 +29,14 @@ export const ContentIcon = ({ type }: { type: ContentType }) => { export const VideoContent = ({ content }: { content: string }) => { const regularVideoId = getYouTubeVideoId(content); const shortsVideoId = getYouTubeShortsId(content); - - if (regularVideoId && !shortsVideoId) { - return ( -
-

- Please upload only YouTube short -

-
- ); - } - - const videoId = shortsVideoId; + const videoId = regularVideoId || shortsVideoId; if (!videoId) return null; return (