diff --git a/messages/en/3d/en_3d.json b/messages/en/3d/en_3d.json index fac92c3dd..26048c4b4 100644 --- a/messages/en/3d/en_3d.json +++ b/messages/en/3d/en_3d.json @@ -42,17 +42,35 @@ "connector": "Connector", "drag_guide": "Drag to add to scene", "double_click_guide": "Double-click to place at origin", - "select_guide": "Select & use gizmo to transform" + "select_guide": "Select & use gizmo to transform", + "bluestraw": "Blue Straw", + "pinkstraw": "Pink Straw", + "yellowstraw": "Yellow Straw", + "greenstraw": "Green Straw", + "orangestraw": "Orange Straw", + "1legconnector": "1 Leg Connector", + "2legsconnector": "2 Legs Connector", + "3legsconnector": "3 Legs Connector", + "5legsconnector": "5 Legs Connector" }, "right_panel": { "workspace_tree": "Workspace Tree", "properties": "Properties", "delete_component": "Do you want to delete this component?", "select_object_or_action": "Select an object or action to edit properties", + "noStep": "No step linked to this action", "action_properties": { "title": "Action Properties", "name": "Name", - "type": "Type" + "type": "Type", + "step": { + "detail": "Step Details", + "title": "Title", + "description": "Description", + "expectedResult": "Expected Result", + "hint": "Hint", + "addHint": "Add Hint" + } }, "component_properties": { "select_object_or_action": "Select an object or action to edit properties", diff --git a/messages/en/admin/en_admin.json b/messages/en/admin/en_admin.json index a6c54547b..d05404935 100644 --- a/messages/en/admin/en_admin.json +++ b/messages/en/admin/en_admin.json @@ -86,6 +86,7 @@ "updateButton": "Update" }, "user": { + "userManagement": "User Management", "createTitle": "Create User", "editTitle": "Edit User", "email": "Email", @@ -145,4 +146,4 @@ "reviewMessage": "Course submission is under review." } } -} +} \ No newline at end of file diff --git a/messages/en/classroom/en_classroom.json b/messages/en/classroom/en_classroom.json index 9e99c10eb..5f67450bc 100644 --- a/messages/en/classroom/en_classroom.json +++ b/messages/en/classroom/en_classroom.json @@ -26,7 +26,7 @@ "startDate": "Start Date", "endDate": "End Date" }, - "curriculum": "Update Classroom Curriculum", + "curriculum": "Update Course", "teacher": "Update Classroom Teacher", "students": { "addStudents": "Add Students to Classroom", @@ -86,7 +86,13 @@ "weeks": "Weeks", "custom": "Custom", "groupList": "Group List", - "grade": "Grade" + "grade": "Grade", + "selectSubscription": "Select Subscription", + "annual": "Annual", + "semiAnnual": "Semi-Annual", + "students": "Students", + "teachers": "Teachers", + "curricula": "Curricula" }, "studentClassroom": { "list": { @@ -112,4 +118,4 @@ "grade": "Grade" } } -} +} \ No newline at end of file diff --git a/messages/en/dashboard/en_dashboard.json b/messages/en/dashboard/en_dashboard.json index 873f22080..f262d7c90 100644 --- a/messages/en/dashboard/en_dashboard.json +++ b/messages/en/dashboard/en_dashboard.json @@ -108,6 +108,56 @@ "currTitle": "Curriculum Title", "name": "Name" } + }, + "admin": { + "title": "System Dashboard", + "subtitle": "Overview of system performance and revenue", + "period": { + "placeholder": "Select period", + "month": "Month", + "quarter": "Quarter", + "year": "Year" + }, + "stats": { + "revenue": "Total Revenue", + "compare": "vs previous period", + "activeOrg": "Active Organizations", + "growth": "Growth", + "totalStudent": "Total Students", + "enrollment": "Enrollments", + "completionRate": "Completion Rate" + }, + "charts": { + "revenueByPlan": "Revenue by Plan", + "revenueDesc": "Revenue distribution based on subscription plans", + "planDetail": "Subscription Plan Details", + "planDetailDesc": "Current subscription plan status", + "totalPlan": "Total Plans", + "active": "Active", + "expired": "Expired", + "noData": "No revenue data available" + }, + "tables": { + "topOrg": "Top Organizations", + "topOrgDesc": "Organizations with the largest student body", + "topCourse": "Top Courses", + "topCourseDesc": "Most popular courses in the system", + "headers": { + "orgName": "Organization Name", + "students": "Students", + "enrollments": "Enrollments", + "passRate": "Pass Rate", + "activeSub": "Active Subs", + "courseName": "Course Name", + "classrooms": "Classrooms", + "avgScore": "Avg Score", + "completion": "Completion" + }, + "placeholders": { + "org": "No organization data", + "course": "No course data" + } + } } } } diff --git a/messages/en/organization/en_organization.json b/messages/en/organization/en_organization.json index 169dd9ad4..b0c559571 100644 --- a/messages/en/organization/en_organization.json +++ b/messages/en/organization/en_organization.json @@ -235,6 +235,7 @@ }, "curriculum": { "curriculum": "Curriculum", + "orgCurriculumDescription": "Your organization currently has access to {count} active curriculum frameworks included in your subscription. Each curriculum provides structured learning pathways, curated courses, and teaching resources designed to support STEM education across different levels. Explore the curricula below to view their courses, timelines, and details.", "title": "Curriculum List", "noData": "No curriculum found for this organization.", "noResultsForFilter": "No results found for the selected filter.", @@ -245,7 +246,12 @@ "filterByStatus": "Filter by Status", "showing": "Showing", "results": "results", - "all": "All" + "all": "All", + "activeSubscriptions": "Active Subscriptions", + "expiredSubscriptions": "Expired Subscriptions", + "upcomingSubscriptions": "Upcoming Subscriptions", + "cancelledSubscriptions": "Cancelled Subscriptions", + "accessPeriod": "Access Period" }, "userTable": { "title": "Manage Users in the Organization", diff --git a/messages/en/subscription/en_subscription.json b/messages/en/subscription/en_subscription.json index 8393d66d1..130997e2c 100644 --- a/messages/en/subscription/en_subscription.json +++ b/messages/en/subscription/en_subscription.json @@ -5,7 +5,7 @@ "searchPlaceholder": "Search organizations by name...", "subscriptionTitle": "Subscription List", "subscriptionHistoryTitle": "Subscription History", - "organizationSubscriptionTitle": "Organization Subscription Management", + "organizationSubscriptionTitle": "Organization Management", "organizationSubscriptionDescription": "Browse and manage all organization subscriptions registered on the platform.", "placeholder": { "search": "Search organizations by name...", diff --git a/messages/vi/3d/vi_3d.json b/messages/vi/3d/vi_3d.json index 895e53796..09ead74e3 100644 --- a/messages/vi/3d/vi_3d.json +++ b/messages/vi/3d/vi_3d.json @@ -42,17 +42,35 @@ "connector": "Đầu nối", "drag_guide": "Kéo để thêm vào cảnh", "double_click_guide": "Nhấp đúp để đặt tại gốc tọa độ", - "select_guide": "Chọn & sử dụng gizmo để biến đổi" + "select_guide": "Chọn & sử dụng gizmo để biến đổi", + "bluestraw": "Ống hút xanh dương", + "pinkstraw": "Ống hút hồng", + "yellowstraw": "Ống hút vàng", + "greenstraw": "Ống hút xanh lá", + "orangestraw": "Ống hút cam", + "1legconnector": "Đầu nối 1 chân", + "2legsconnector": "Đầu nối 2 chân", + "3legsconnector": "Đầu nối 3 chân", + "5legsconnector": "Đầu nối 5 chân" }, "right_panel": { "workspace_tree": "Cấu trúc mô hình", "properties": "Thuộc tính", "delete_component": "Bạn có muốn xóa thành phần này không?", "select_object_or_action": "Chọn một vật thể hoặc hành động để chỉnh sửa thuộc tính", + "noStep": "Không có bước nào liên kết với hành động này", "action_properties": { "title": "Thuộc tính hành động", "name": "Tên", - "type": "Loại" + "type": "Loại", + "step": { + "detail": "Chi tiết bước", + "title": "Tiêu đề", + "description": "Mô tả", + "expectedResult": "Kết quả mong đợi", + "hint": "Gợi ý", + "addHint": "Thêm gợi ý" + } }, "component_properties": { "select_object_or_action": "Chọn một vật thể hoặc hành động để chỉnh sửa thuộc tính", diff --git a/messages/vi/admin/vi_admin.json b/messages/vi/admin/vi_admin.json index ecc3f365b..c943fb631 100644 --- a/messages/vi/admin/vi_admin.json +++ b/messages/vi/admin/vi_admin.json @@ -87,6 +87,7 @@ "updateButton": "Cập nhật" }, "user": { + "userManagement": "Quản lý người dùng", "createTitle": "Tạo người dùng", "editTitle": "Chỉnh sửa người dùng", "email": "Email", @@ -146,4 +147,4 @@ "reviewMessage": "Khóa học đang đợi quản trị viên phê duyệt." } } -} +} \ No newline at end of file diff --git a/messages/vi/classroom/vi_classroom.json b/messages/vi/classroom/vi_classroom.json index 66f8ada17..fcfbd7853 100644 --- a/messages/vi/classroom/vi_classroom.json +++ b/messages/vi/classroom/vi_classroom.json @@ -25,7 +25,7 @@ "startDate": "Ngày bắt đầu", "endDate": "Ngày kết thúc" }, - "curriculum": "Cập nhật chương trình học của lớp học", + "curriculum": "Cập nhật khóa học", "teacher": "Cập nhật giáo viên của lớp học", "students": { "addStudents": "Thêm học sinh vào lớp học", @@ -85,7 +85,13 @@ "description": "Mô tả", "descriptionPlaceholder": "Mô tả ngắn gọn về lớp học này...", "groupList": "Danh sách nhóm", - "grade": "Khối" + "grade": "Khối", + "selectSubscription": "Chọn gói đăng ký", + "annual": "Hàng năm", + "semiAnnual": "Nửa năm", + "students": "Học sinh", + "teachers": "Giáo viên", + "curricula": "Chương trình học" }, "studentClassroom": { "list": { @@ -111,4 +117,4 @@ "grade": "Khối" } } -} +} \ No newline at end of file diff --git a/messages/vi/dashboard/vi_dashboard.json b/messages/vi/dashboard/vi_dashboard.json index 36cc33b7d..e3d08f895 100644 --- a/messages/vi/dashboard/vi_dashboard.json +++ b/messages/vi/dashboard/vi_dashboard.json @@ -109,6 +109,56 @@ "currTitle": "Tên chương trình", "name": "Tên" } + }, + "admin": { + "title": "Bảng điều khiển hệ thống", + "subtitle": "Tổng quan hiệu suất hệ thống và doanh thu", + "period": { + "placeholder": "Chọn kỳ", + "month": "Tháng này", + "quarter": "Quý này", + "year": "Năm này" + }, + "stats": { + "revenue": "Tổng doanh thu", + "compare": "So với kỳ trước", + "activeOrg": "Tổ chức hoạt động", + "growth": "Tăng trưởng", + "totalStudent": "Tổng học sinh", + "enrollment": "Lượt ghi danh", + "completionRate": "Tỷ lệ hoàn thành" + }, + "charts": { + "revenueByPlan": "Doanh thu theo Gói", + "revenueDesc": "Phân bổ doanh thu dựa trên các gói đăng ký", + "planDetail": "Chi tiết gói đăng ký", + "planDetailDesc": "Trạng thái các gói đăng ký hiện tại", + "totalPlan": "Tổng gói", + "active": "Đang hoạt động", + "expired": "Đã hết hạn", + "noData": "Chưa có dữ liệu doanh thu" + }, + "tables": { + "topOrg": "Top Tổ Chức", + "topOrgDesc": "Các tổ chức có lượng học viên lớn nhất", + "topCourse": "Top Khóa Học", + "topCourseDesc": "Các khóa học phổ biến nhất hệ thống", + "headers": { + "orgName": "Tên Tổ Chức", + "students": "Học sinh", + "enrollments": "Ghi danh", + "passRate": "Tỷ lệ đạt", + "activeSub": "Gói hoạt động", + "courseName": "Khóa học", + "classrooms": "Lớp học", + "avgScore": "Điểm TB", + "completion": "Hoàn thành" + }, + "placeholders": { + "org": "Chưa có dữ liệu tổ chức", + "course": "Chưa có dữ liệu khóa học" + } + } } } } diff --git a/messages/vi/organization/vi_organization.json b/messages/vi/organization/vi_organization.json index e8a317c1c..5fdef5219 100644 --- a/messages/vi/organization/vi_organization.json +++ b/messages/vi/organization/vi_organization.json @@ -234,6 +234,7 @@ }, "curriculum": { "curriculum": "Khung chương trình", + "orgCurriculumDescription": "Tổ chức của bạn hiện đang được cấp quyền truy cập vào {count} khung chương trình thuộc gói đăng ký.\nMỗi khung chương trình cung cấp lộ trình học tập rõ ràng, danh mục khóa học và tài nguyên giảng dạy nhằm hỗ trợ việc triển khai giáo dục STEM ở nhiều cấp độ. Hãy khám phá các chương trình bên dưới để xem thông tin khóa học, thời gian học và chi tiết liên quan.", "title": "Danh sách khung chương trình", "noData": "Không tìm thấy chương trình giảng dạy nào cho tổ chức này.", "noResultsForFilter": "Không tìm thấy kết quả nào cho bộ lọc đã chọn.", @@ -244,7 +245,12 @@ "filterByStatus": "Lọc theo trạng thái", "showing": "Hiển thị", "results": "kết quả", - "all": "Tất cả" + "all": "Tất cả", + "activeSubscriptions": "Gói đang hoạt động", + "expiredSubscriptions": "Gói đã hết hạn", + "upcomingSubscriptions": "Gói sắp kích hoạt", + "cancelledSubscriptions": "Gói đã hủy", + "accessPeriod": "Thời hạn truy cập" }, "userTable": { "title": "Quản lý người dùng trong Tổ chức", diff --git a/messages/vi/subscription/vi_subscription.json b/messages/vi/subscription/vi_subscription.json index fea81fd77..65a848fe5 100644 --- a/messages/vi/subscription/vi_subscription.json +++ b/messages/vi/subscription/vi_subscription.json @@ -5,7 +5,7 @@ "searchPlaceholder": "Tìm kiếm tổ chức theo tên...", "subscriptionTitle": "Danh sách gói đăng ký", "subscriptionHistoryTitle": "Lịch sử đăng ký", - "organizationSubscriptionTitle": "Quản lý đăng ký tổ chức", + "organizationSubscriptionTitle": "Quản lý tổ chức", "organizationSubscriptionDescription": "Duyệt và quản lý tất cả các đăng ký tổ chức đã đăng ký trên nền tảng.", "placeholder": { "search": "Tìm kiếm tổ chức theo tên...", diff --git a/public/components/templates/ConnectorTypes/1leg.json b/public/components/templates/ConnectorTypes/1leg.json index f1cbdd37c..a57679777 100644 --- a/public/components/templates/ConnectorTypes/1leg.json +++ b/public/components/templates/ConnectorTypes/1leg.json @@ -1,6 +1,6 @@ { "id": "1leg", - "name": "1-Leg Red Connector", + "name": "Đầu nối 1 chân", "version": "1.0", "category": "connector", "baseGeometry": { diff --git a/public/components/templates/ConnectorTypes/2legs.json b/public/components/templates/ConnectorTypes/2legs.json index a50b4c38d..abb9ae990 100644 --- a/public/components/templates/ConnectorTypes/2legs.json +++ b/public/components/templates/ConnectorTypes/2legs.json @@ -1,6 +1,6 @@ { "id": "2leg", - "name": "2-Leg Connector", + "name": "Đầu nối 2 chân", "version": "1.0", "category": "connector", "baseGeometry": { diff --git a/public/components/templates/ConnectorTypes/3legs.json b/public/components/templates/ConnectorTypes/3legs.json index 7809f075e..0c399bde0 100644 --- a/public/components/templates/ConnectorTypes/3legs.json +++ b/public/components/templates/ConnectorTypes/3legs.json @@ -1,6 +1,6 @@ { "id": "3leg", - "name": "3-Leg Red Connector", + "name": "Đầu nối 3 chân", "version": "1.0", "category": "connector", "baseGeometry": { diff --git a/public/components/templates/ConnectorTypes/5legs.json b/public/components/templates/ConnectorTypes/5legs.json index 38c4fd82d..76e2d2af6 100644 --- a/public/components/templates/ConnectorTypes/5legs.json +++ b/public/components/templates/ConnectorTypes/5legs.json @@ -1,6 +1,6 @@ { "id": "5leg", - "name": "5-Leg Red Connector", + "name": "Đầu nối 5 chân", "version": "1.0", "category": "connector", "baseGeometry": { diff --git a/public/components/templates/StrawTypes/blue_19_0.json b/public/components/templates/StrawTypes/blue_19_0.json index e248b5146..fa614de34 100644 --- a/public/components/templates/StrawTypes/blue_19_0.json +++ b/public/components/templates/StrawTypes/blue_19_0.json @@ -1,7 +1,7 @@ { "id": "blue_19_0", - "shortName": "Blue Straw", - "name": "Blue Straw 19.0cm", + "shortName": "Ống hút xanh dương", + "name": "Ống hút xanh dương", "version": "1.0", "category": "straw", "baseGeometry": { diff --git a/public/components/templates/StrawTypes/green_11_2.json b/public/components/templates/StrawTypes/green_11_2.json index 2877cbab4..7576dd3a5 100644 --- a/public/components/templates/StrawTypes/green_11_2.json +++ b/public/components/templates/StrawTypes/green_11_2.json @@ -1,7 +1,7 @@ { "id": "green_11_2", - "shortName": "Green Straw", - "name": "Green Straw 15.0cm", + "shortName": "Ống hút xanh lá", + "name": "Ống hút xanh lá", "version": "1.0", "category": "straw", "baseGeometry": { diff --git a/public/components/templates/StrawTypes/green_16_2.json b/public/components/templates/StrawTypes/green_16_2.json index 78be47068..7004356bd 100644 --- a/public/components/templates/StrawTypes/green_16_2.json +++ b/public/components/templates/StrawTypes/green_16_2.json @@ -1,7 +1,7 @@ { "id": "green_16_2", - "shortName": "Green Straw", - "name": "Green Straw 16.2cm", + "shortName": "Ống hút xanh lá", + "name": "Ống hút xanh lá", "version": "1.0", "category": "straw", "baseGeometry": { diff --git a/public/components/templates/StrawTypes/orange_6_3.json b/public/components/templates/StrawTypes/orange_6_3.json index 0dadbff05..8f4bd8dba 100644 --- a/public/components/templates/StrawTypes/orange_6_3.json +++ b/public/components/templates/StrawTypes/orange_6_3.json @@ -1,7 +1,7 @@ { "id": "orange_6_3", - "shortName": "Orange Straw", - "name": "Orange Straw 6.3cm", + "shortName": "Ống hút cam", + "name": "Ống hút cam", "version": "1.0", "category": "straw", "baseGeometry": { diff --git a/public/components/templates/StrawTypes/pink_8_9.json b/public/components/templates/StrawTypes/pink_8_9.json index 916958748..587cb3070 100644 --- a/public/components/templates/StrawTypes/pink_8_9.json +++ b/public/components/templates/StrawTypes/pink_8_9.json @@ -1,7 +1,7 @@ { "id": "pink_8_9", - "shortName": "Pink Straw", - "name": "Pink Straw 8.9cm", + "shortName": "Ống hút hồng", + "name": "Ống hút hồng", "version": "1.0", "category": "straw", "baseGeometry": { diff --git a/public/components/templates/StrawTypes/yellow_3_8.json b/public/components/templates/StrawTypes/yellow_3_8.json index a8a3f9fde..d6b96f8e7 100644 --- a/public/components/templates/StrawTypes/yellow_3_8.json +++ b/public/components/templates/StrawTypes/yellow_3_8.json @@ -1,7 +1,7 @@ { "id": "yellow_3_8", - "shortName": "Yellow Straw", - "name": "Yellow Straw 3.8cm", + "shortName": "Ống hút vàng", + "name": "Ống hút vàng", "version": "1.0", "category": "straw", "baseGeometry": { diff --git a/src/app/[locale]/admin/(main)/dashboard/page.tsx b/src/app/[locale]/admin/(main)/dashboard/page.tsx index d9c7d481c..a6c2f64cf 100644 --- a/src/app/[locale]/admin/(main)/dashboard/page.tsx +++ b/src/app/[locale]/admin/(main)/dashboard/page.tsx @@ -1,10 +1,9 @@ -import data from './data.json' -import { DataTable } from '@/components/shadcn/data-table' +import SystemDashboardPage from '@/features/dashboard/components/AdminSystemDashboard' -export default function Page() { +export default function AdminDashboardPage() { return (
- +
) } diff --git a/src/app/[locale]/test2/page.tsx b/src/app/[locale]/test2/page.tsx index 7c34db2e5..4f85e419a 100644 --- a/src/app/[locale]/test2/page.tsx +++ b/src/app/[locale]/test2/page.tsx @@ -3,1341 +3,54 @@ import React from 'react' export default function Page() { const jsonObject = { metadata: { - title: 'Assembly_emu_e3676a7ca85a', - description: 'Exported assembly JSON', + title: 'Assembly emu_fc1f61b54ebf', + description: 'Exported from workspace', author: 'STEMify User', version: '2.0', - created: '2025-12-04T08:00:41.963Z', - lastModified: '2025-12-04T08:00:41.963Z' + created: '2025-12-11T07:16:28.516Z', + lastModified: '2025-12-11T07:16:28.516Z' }, templates: { materials: [ - { - id: 'plastic_green', - source: '/components/templates/MaterialLibrary/plastic_green.json' - }, - { - id: 'plastic_red', - source: '/components/templates/MaterialLibrary/plastic_red.json' - }, - { - id: 'plastic_blue', - source: '/components/templates/MaterialLibrary/plastic_blue.json' - }, - { - id: 'plastic_yellow', - source: '/components/templates/MaterialLibrary/plastic_yellow.json' - }, - { - id: 'plastic_orange', - source: '/components/templates/MaterialLibrary/plastic_orange.json' - }, - { - id: 'plastic_pink', - source: '/components/templates/MaterialLibrary/plastic_pink.json' - } + { id: 'plastic_green', source: '/components/templates/MaterialLibrary/plastic_green.json' }, + { id: 'plastic_red', source: '/components/templates/MaterialLibrary/plastic_red.json' }, + { id: 'plastic_blue', source: '/components/templates/MaterialLibrary/plastic_blue.json' }, + { id: 'plastic_yellow', source: '/components/templates/MaterialLibrary/plastic_yellow.json' }, + { id: 'plastic_orange', source: '/components/templates/MaterialLibrary/plastic_orange.json' }, + { id: 'plastic_pink', source: '/components/templates/MaterialLibrary/plastic_pink.json' } ], components: [ - { - id: 'blue_19_0', - source: '/components/templates/StrawTypes/blue_19_0.json' - }, - { - id: 'green_16_2', - source: '/components/templates/StrawTypes/green_16_2.json' - }, - { - id: 'pink_8_9', - source: '/components/templates/StrawTypes/pink_8_9.json' - }, - { - id: 'orange_6_3', - source: '/components/templates/StrawTypes/orange_6_3.json' - }, - { - id: 'yellow_3_8', - source: '/components/templates/StrawTypes/yellow_3_8.json' - }, - { - id: '1leg', - source: '/components/templates/ConnectorTypes/1leg.json' - }, - { - id: '2leg', - source: '/components/templates/ConnectorTypes/2legs.json' - }, - { - id: '3leg', - source: '/components/templates/ConnectorTypes/3legs.json' - }, - { - id: '5leg', - source: '/components/templates/ConnectorTypes/5legs.json' - } + { id: 'blue_19_0', source: '/components/templates/StrawTypes/blue_19_0.json' }, + { id: 'green_16_2', source: '/components/templates/StrawTypes/green_16_2.json' }, + { id: 'pink_8_9', source: '/components/templates/StrawTypes/pink_8_9.json' }, + { id: 'orange_6_3', source: '/components/templates/StrawTypes/orange_6_3.json' }, + { id: 'yellow_3_8', source: '/components/templates/StrawTypes/yellow_3_8.json' }, + { id: '1leg', source: '/components/templates/ConnectorTypes/1leg.json' }, + { id: '2leg', source: '/components/templates/ConnectorTypes/2legs.json' }, + { id: '3leg', source: '/components/templates/ConnectorTypes/3legs.json' }, + { id: '5leg', source: '/components/templates/ConnectorTypes/5legs.json' } ] }, instances: { - straws: [ - { - templateId: 'orange_6_3', - instances: [ - { - id: 'straw_2', - transform: { - position: { - x: -16.81, - y: 0, - z: 0.5 - }, - rotation: { - x: 0, - y: 1.5707963267948966, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_6', - transform: { - position: { - x: 1.2, - y: 0, - z: 0.1 - }, - rotation: { - x: 0, - y: 1.5707963267948966, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_9', - transform: { - position: { - x: 19.58, - y: 0, - z: 0.1 - }, - rotation: { - x: 0, - y: 1.5707963267948966, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - } - ] - }, - { - templateId: 'blue_19_0', - instances: [ - { - id: 'straw_5', - transform: { - position: { - x: -7.9, - y: 0, - z: 0.7 - }, - rotation: { - x: 0, - y: 0.40142572795869574, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_8', - transform: { - position: { - x: 10.3, - y: 0, - z: 0.2 - }, - rotation: { - x: 0, - y: 0.40142572795869574, - z: 0.011693705988362009 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_23', - transform: { - position: { - x: 25.37, - y: 8.05, - z: -7.242924715885387 - }, - rotation: { - x: 0, - y: 0, - z: 1.0471975511965976 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_24', - transform: { - position: { - x: 34.77249725174951, - y: 8.1, - z: -7.2 - }, - rotation: { - x: 0, - y: 0, - z: -1.0471975511965976 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_25', - transform: { - position: { - x: 30.31080251771205, - y: 0, - z: -7.170156821198586 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_26', - transform: { - position: { - x: 30.3, - y: 0, - z: 9 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_27', - transform: { - position: { - x: 25.3, - y: 8.1, - z: 8.999041192800192 - }, - rotation: { - x: 0, - y: 0, - z: 1.0471975511965976 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_28', - transform: { - position: { - x: 34.86959286657108, - y: 8.1, - z: 8.951652990328856 - }, - rotation: { - x: 0, - y: 0, - z: -1.0471975511965976 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_31', - transform: { - position: { - x: 34.81607758931416, - y: 8.45, - z: 1.7943938040032492 - }, - rotation: { - x: 0, - y: 0, - z: -1.0471975511965976 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_32', - transform: { - position: { - x: 19.093986291090562, - y: 8.90132410410023, - z: 0.2544576705499644 - }, - rotation: { - x: 0, - y: 0, - z: -0.5235987755982988 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_35', - transform: { - position: { - x: 1.7507498195927065, - y: 13.354867557265095, - z: 0.29941936665361224 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - } - ] - }, - { - templateId: 'green_16_2', - instances: [ - { - id: 'straw_10', - transform: { - position: { - x: -7.7, - y: 0, - z: -3.8 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_11', - transform: { - position: { - x: 10.4, - y: 0, - z: -3.8 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_12', - transform: { - position: { - x: -7.7, - y: 0, - z: 4.7 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_13', - transform: { - position: { - x: 10.4, - y: 0, - z: 4.7 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_14', - transform: { - position: { - x: -12.1, - y: 6.5, - z: -1.8 - }, - rotation: { - x: 0, - y: -0.4537856055185257, - z: 0.8726646259971648 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_15', - transform: { - position: { - x: -12.1, - y: 6.5, - z: 2.3 - }, - rotation: { - x: 0, - y: 0.41887902047863906, - z: 0.8726646259971648 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_16', - transform: { - position: { - x: -3.330841863339714, - y: 6.248445372099425, - z: -1.9038064789070885 - }, - rotation: { - x: 0, - y: 0.47123889803846897, - z: -0.9075712110370514 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_17', - transform: { - position: { - x: -3.2, - y: 6.47, - z: 2.26 - }, - rotation: { - x: 0, - y: -0.4363323129985824, - z: -0.890117918517108 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_18', - transform: { - position: { - x: 6.3, - y: 6.5, - z: -1.8 - }, - rotation: { - x: 0, - y: -0.4537856055185257, - z: 0.8726646259971648 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_19', - transform: { - position: { - x: 6.3, - y: 6.5, - z: 2.3 - }, - rotation: { - x: 0, - y: 0.41887902047863906, - z: 0.8726646259971648 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_20', - transform: { - position: { - x: 15.33, - y: 6.5, - z: -1.9 - }, - rotation: { - x: 0, - y: 0.47123889803846897, - z: -0.9075712110370514 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_21', - transform: { - position: { - x: 15.3, - y: 6.5, - z: 2.5 - }, - rotation: { - x: 0, - y: -0.4363323129985824, - z: -0.890117918517108 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_22', - transform: { - position: { - x: 20.722631586967886, - y: 0, - z: 1.0057328801081784 - }, - rotation: { - x: 0, - y: 1.5707963267948966, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_29', - transform: { - position: { - x: 30.0045944833505, - y: 16.5, - z: 0.8561350731218518 - }, - rotation: { - x: 0, - y: 1.5707963267948966, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - }, - { - id: 'straw_30', - transform: { - position: { - x: 40.057301867781874, - y: 0, - z: 1.1100846063914593 - }, - rotation: { - x: 0, - y: 1.5707963267948966, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - } - ] - }, - { - templateId: 'yellow_3_8', - instances: [ - { - id: 'straw_34', - transform: { - position: { - x: 26.17041960671652, - y: 2.1148457882507943, - z: 0.5329097101846596 - }, - rotation: { - x: 0, - y: 0, - z: -2.2689280275926285 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - } - } - ] - } - ], + straws: [], connectors: [ - { - templateId: '2leg', - instances: [ - { - id: 'connector_5', - transform: { - position: { - x: -16.8, - y: 0, - z: -3.1 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_7', - transform: { - position: { - x: -16.9, - y: 0, - z: 4.1 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_14', - transform: { - position: { - x: 1.3, - y: 0, - z: -3.7 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_15', - transform: { - position: { - x: 1.3, - y: 0, - z: 3.8 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_18', - transform: { - position: { - x: 19.66, - y: 0, - z: -3.15 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_20', - transform: { - position: { - x: 19.5, - y: 0, - z: 3.8 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_23', - transform: { - position: { - x: 1.2, - y: 0, - z: -3.7 - }, - rotation: { - x: 1.5707963267948966, - y: 3.141592653589793, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_27', - transform: { - position: { - x: 1.3, - y: 0, - z: 4.7 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - } - ] - }, { templateId: '1leg', instances: [ { - id: 'connector_9', - transform: { - position: { - x: -16.97, - y: 0, - z: 4.5 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: -0.3490658503988659 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_11', - transform: { - position: { - x: 1.3, - y: 0, - z: -3.34 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 2.8099800957108703 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_16', - transform: { - position: { - x: 1.2, - y: 0, - z: 4.3 - }, - rotation: { - x: 1.5707963267948966, - y: -0.03490658503988659, - z: -0.6108652381980153 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_19', - transform: { - position: { - x: 19.74, - y: 0, - z: -3.97 - }, - rotation: { - x: 1.5707963267948966, - y: 3.141592653589793, - z: 0.4363323129985824 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_21', - transform: { - position: { - x: -16.8, - y: 0, - z: -3.8 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_25', - transform: { - position: { - x: -17, - y: 0, - z: 4.6 - }, - rotation: { - x: 1.5707963267948966, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_29', - transform: { - position: { - x: 19.41, - y: -0.013953688090604155, - z: 4.76 - }, - rotation: { - x: 1.5707963267948966, - y: 3.141592653589793, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_30', - transform: { - position: { - x: 19.5, - y: 0, - z: -3.9 - }, - rotation: { - x: 1.5707963267948966, - y: 3.141592653589793, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_31', - transform: { - position: { - x: -7.6, - y: 13.3, - z: 0.3 - }, - rotation: { - x: 0, - y: 0, - z: -1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_32', - transform: { - position: { - x: 10.94, - y: 13.6, - z: 0.4 - }, - rotation: { - x: 0, - y: 0, - z: -1.5707963267948966 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_33', - transform: { - position: { - x: 20.74784905648725, - y: 0, - z: -3.496217776302761 - }, - rotation: { - x: 1.5707963267948966, - y: 3.141592653589793, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_34', + id: 'connector_1', transform: { - position: { - x: 20.79, - y: 0, - z: 3.687578897905908 - }, - rotation: { - x: 1.5707963267948966, - y: 3.141592653589793, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } + position: { x: -0.6475473374387839, y: 0, z: 1.0008010201402797 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 } }, - arms: {} - }, - { - id: 'connector_35', - transform: { - position: { - x: 30.073391112008878, - y: 16.68, - z: 1.7167804549312669 - }, - rotation: { - x: 1.5707963267948966, - y: -1.0471975511965976, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_36', - transform: { - position: { - x: 39.83283639272023, - y: 0, - z: 1.7928342987420356 - }, - rotation: { - x: 1.5707963267948966, - y: 2.0943951023931953, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_37', - transform: { - position: { - x: 27.8646836486829, - y: 4.1066451023783985, - z: 0.5060142278475936 - }, - rotation: { - x: 1.5707963267948966, - y: -2.2689280275926285, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: {} - }, - { - id: 'connector_38', - transform: { - position: { - x: 10.558742342045136, - y: 0, - z: 17.134284182871355 - }, - rotation: { - x: 0, - y: 0, - z: 0 - }, - scale: { - x: 1, - y: 1, - z: 1 - } - }, - arms: { - arm_1: { - x: 0, - y: 0, - z: 0 - }, - arm_2: { - x: 0, - y: 0, - z: 0 - }, - arm_3: { - x: 0, - y: 0, - z: 0 - } - } + arms: { arm_1: { x: 0, y: 0, z: 0 }, arm_2: { x: 0, y: 0, z: 0 }, arm_3: { x: 0, y: 0, z: 0 } } } ] } ] }, - actions: [ - { - id: 'action_1', - name: 'Default Action', - type: 'highlight', - targets: [ - 'straw_2', - 'straw_5', - 'connector_5', - 'connector_7', - 'connector_9', - 'connector_11', - 'straw_6', - 'connector_14', - 'connector_15', - 'straw_8', - 'connector_16', - 'straw_9', - 'connector_18', - 'connector_19', - 'connector_20' - ], - duration: 2 - }, - { - id: 'action_2', - name: 'Highlight Action 2', - type: 'highlight', - targets: [ - 'straw_10', - 'connector_21', - 'straw_11', - 'connector_23', - 'straw_12', - 'connector_25', - 'connector_27', - 'straw_13', - 'connector_29', - 'connector_30' - ], - duration: 2 - }, - { - id: 'action_3', - name: 'Highlight Action 3', - type: 'highlight', - targets: ['straw_14', 'straw_15', 'straw_16', 'straw_17', 'connector_31'], - duration: 2 - }, - { - id: 'action_4', - name: 'Highlight Action 4', - type: 'highlight', - targets: ['straw_18', 'straw_19', 'straw_20', 'straw_21', 'connector_32'], - duration: 2 - }, - { - id: 'action_5', - name: 'Highlight Action 5', - type: 'highlight', - targets: ['straw_22', 'connector_33', 'connector_34'], - duration: 2 - }, - { - id: 'action_6', - name: 'Highlight Action 6', - type: 'highlight', - targets: ['straw_23', 'straw_24', 'straw_25', 'straw_26', 'straw_27', 'straw_28'], - duration: 2 - }, - { - id: 'action_7', - name: 'Highlight Action 7', - type: 'highlight', - targets: ['straw_29', 'connector_35'], - duration: 2 - }, - { - id: 'action_8', - name: 'Highlight Action 8', - type: 'highlight', - targets: ['straw_30', 'connector_36'], - duration: 2 - }, - { - id: 'action_9', - name: 'Highlight Action 9', - type: 'highlight', - targets: ['straw_31'], - duration: 2 - }, - { - id: 'action_10', - name: 'Highlight Action 10', - type: 'highlight', - targets: ['straw_32', 'straw_34', 'connector_37', 'straw_35'], - duration: 2 - }, - { - id: 'action_11', - name: 'Highlight Action 10', - type: 'highlight', - targets: ['connector_38'], - duration: 2 - } - ], + actions: [{ id: 'action_1', name: 'Bước 1', type: 'highlight', targets: ['connector_1'], duration: 2 }], activities: [ { id: 'custom_assembly', @@ -1346,135 +59,8 @@ export default function Page() { difficulty: 'beginner', estimatedTime: 600, steps: [ - { - actionId: 'action_11', - title: 'Khởi Động Cầu Nâng Thông Minh', - description: 'Hãy nhấn nút tiếp tục bắt đầu hành trình sáng tạo của bạn!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_1', - title: 'Tạo Dầm Cầu Ziczac', - description: - 'Lắp các ống hút nối nhau như hình ziczac nhé! Cây cầu sẽ trông thật ngầu và chắc chắn, sẵn sàng cho phần nâng hạ ở bước tiếp theo!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_2', - title: 'Hoàn Thiện Mặt Cầu', - description: - 'Tạo các thanh dài và gắn chúng quanh khung ziczac nào! Giờ thì cây cầu của bạn đã có phần nền cực kỳ vững chắc rồi đấy!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_3', - title: 'Lắp Các Thanh Nối Vào Khung Cầu', - description: - 'Tạo các thanh nối chắc chắn rồi gắn chúng vào phần khung cầu nhé! Những thanh nối này sẽ giúp cây cầu thêm bền vững và sẵn sàng cho cơ chế nâng hạ hoạt động trơn tru.', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_4', - title: 'Làm Lại Một Lần Nữa!', - description: 'Lặp lại bước trước ở bên còn lại nhé! Cây cầu của bạn sắp hoàn thiện rồi — thật tuyệt vời!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_5', - title: 'Tạo Thanh Trượt Cho Cầu Nâng', - description: - 'Hãy lắp một thanh trượt và đặt nó dọc theo khung cầu. Thanh này giúp mô phỏng cơ chế trượt – nơi cây cầu có thể di chuyển mượt mà khi được nâng lên hoặc hạ xuống.', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_6', - title: 'Dựng Hai Khung Tam Giác Siêu Chắc!', - description: - 'Hãy lắp các thanh nối thành hình tam giác ở hai bên cây cầu. Hình tam giác là cấu trúc vững chắc nhất trong kỹ thuật xây dựng, giúp cây cầu của bạn đứng vững và chịu lực tốt hơn!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_7', - title: 'Gắn Ống Hút Nối 2 Khung Tam Giác', - description: - 'Lắp các ống hút vào hình tam giác nhé, rồi trượt connector vào đầu ống như đang “mặc áo giáp” cho cầu vậy! Giờ cây cầu của bạn trông thật vững chãi và cực kỳ ngầu!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_8', - title: 'Làm Lại Ở Bên Còn Lại Nào!', - description: - 'Lặp lại bước trước ở phía bên kia nhé! Khi hoàn thành, cây cầu của bạn sẽ trông thật cân đối và sẵn sàng nâng lên – hạ xuống như một cây cầu thật!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_9', - title: 'Làm Thanh Chống Siêu Vững Cho Cầu!', - description: - 'ắn các khớp nối vào ống hút để tạo thanh chống nhé! Vậy là cây cầu của bạn đã có “cánh tay đỡ” cực kỳ mạnh mẽ!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_10', - title: 'Lắp Đòn Bẩy Thần Kỳ!', - description: - 'Hãy gắn một ống hút làm đòn bẩy ở phần chân cầu nhé! Khi bạn ấn xuống, cây cầu sẽ nâng lên – giống như phép thuật vậy! Đã đến lúc thử nghiệm mô hình của bạn!', - expectedResult: 'Targets are highlighted', - hints: null, - validation: null - }, - { - actionId: 'action_12', - title: 'Transform Step 12', - description: '', - expectedResult: '', - hints: [], - validation: null - }, - { - actionId: 'action_13', - title: 'Highlight Step 13', - description: '', - expectedResult: '', - hints: [], - validation: null - }, - { - actionId: 'action_14', - title: 'Highlight Step 14', - description: '', - expectedResult: '', - hints: [], - validation: null - }, - { - actionId: 'action_15', - title: 'Highlight Step 15', - description: '', - expectedResult: '', - hints: [], - validation: null - } + { actionId: 'action_1', title: 'Bước 1', description: '', expectedResult: '', hints: [], validation: null }, + { title: 'Bước 1', actionId: 'action_1', description: '', expectedResult: '', hints: [] } ] } ], @@ -1483,49 +69,13 @@ export default function Page() { background: '#f5f5f5', lighting: { ambient: '#404040', - directional: { - color: '#FFFFFF', - intensity: 1.2, - position: { - x: 10, - y: 15, - z: 8 - } - } + directional: { color: '#FFFFFF', intensity: 1.2, position: { x: 10, y: 15, z: 8 } } }, - camera: { - position: { - x: 30, - y: 20, - z: 30 - }, - target: { - x: 0, - y: 0, - z: 0 - }, - fov: 60, - controls: 'orbit' - } + camera: { position: { x: 30, y: 20, z: 30 }, target: { x: 0, y: 0, z: 0 }, fov: 60, controls: 'orbit' } }, workspace: { - bounds: { - min: { - x: -100, - y: -100, - z: -100 - }, - max: { - x: 100, - y: 100, - z: 100 - } - }, - grid: { - visible: true, - size: 1, - divisions: 100 - } + bounds: { min: { x: -100, y: -100, z: -100 }, max: { x: 100, y: 100, z: 100 } }, + grid: { visible: true, size: 1, divisions: 100 } } } } diff --git a/src/components/shadcn/checkbox.tsx b/src/components/shadcn/checkbox.tsx index 0131f2d6e..83de8a51d 100644 --- a/src/components/shadcn/checkbox.tsx +++ b/src/components/shadcn/checkbox.tsx @@ -11,7 +11,7 @@ function Checkbox({ className, ...props }: React.ComponentProps(false) - const [uploadProgress, setUploadProgress] = useState(0) - const [isUploading, setIsUploading] = useState(false) + const [internalFile, setInternalFile] = useState(null) const fileInputRef = useRef(null) @@ -90,20 +89,6 @@ export default function UploadCSV({ } else { setInternalFile(newFile) } - - setIsUploading(true) - setUploadProgress(0) - - const interval = setInterval(() => { - setUploadProgress((prev) => { - if (prev >= 100) { - clearInterval(interval) - setIsUploading(false) - return 100 - } - return prev + 10 - }) - }, 200) } const handleRemoveFile = () => { @@ -113,18 +98,11 @@ export default function UploadCSV({ setInternalFile(null) } - setUploadProgress(0) - setIsUploading(false) if (fileInputRef.current) { fileInputRef.current.value = '' } } - const handleCancel = () => { - setIsUploading(false) - setUploadProgress(0) - } - const formatFileSize = (bytes: number): string => { return `${(bytes / 1024).toFixed(0)} KB` } @@ -176,7 +154,7 @@ dattse18@gmail.com,Dat,Tran,Student,StudentLic,STEM06,S006` className={`rounded-lg border-2 border-dashed p-8 text-center transition-all ${ isDragging ? 'border-blue-500 bg-blue-50' - : uploadedFile && !isUploading + : uploadedFile ? 'border-green-500 bg-green-50' : 'border-gray-300 bg-gray-50' }`} @@ -185,34 +163,7 @@ dattse18@gmail.com,Dat,Tran,Student,StudentLic,STEM06,S006` onDragLeave={handleDragLeave} onDrop={handleDrop} > - {isUploading ? ( -
-
- - - - -
- {uploadProgress}% -
-
-

{to('uploading')}

- -
- ) : uploadedFile ? ( + {uploadedFile ? (
@@ -245,7 +196,7 @@ dattse18@gmail.com,Dat,Tran,Student,StudentLic,STEM06,S006` )}
- {uploadedFile && !isUploading && ( + {uploadedFile && (
@@ -261,29 +212,6 @@ dattse18@gmail.com,Dat,Tran,Student,StudentLic,STEM06,S006`
)} - - {isUploading && uploadedFile && ( -
-
-
-
- -
-
-

{uploadedFile.name}

- {uploadedFile.size &&

{formatFileSize(uploadedFile.size)}

} -
-
- {uploadProgress}% -
-
-
-
-
- )}
) } diff --git a/src/features/assembly/hooks/useAssemblyOptimized.ts b/src/features/assembly/hooks/useAssemblyOptimized.ts index 05a78fc6a..c255d1068 100644 --- a/src/features/assembly/hooks/useAssemblyOptimized.ts +++ b/src/features/assembly/hooks/useAssemblyOptimized.ts @@ -378,7 +378,6 @@ export function useAssembly(options: UseAssemblyOptions = {}): UseAssemblyReturn let assemblyData: Assembly | null = null - // ✅ Ưu tiên dùng inline data (từ API) if (inlineData) { assemblyData = inlineData } diff --git a/src/features/classroom/components/detail/OrganizationClassroomDetail.tsx b/src/features/classroom/components/detail/OrganizationClassroomDetail.tsx index 56684da2b..a263d553a 100644 --- a/src/features/classroom/components/detail/OrganizationClassroomDetail.tsx +++ b/src/features/classroom/components/detail/OrganizationClassroomDetail.tsx @@ -1,7 +1,7 @@ 'use client' import { useDeleteClassroomStudentsMutation, useGetClassroomByIdQuery } from '@/features/classroom/api/classroomApi' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { Card, CardContent, CardHeader, CardTitle } from '@/components/shadcn/card' import { Badge } from '@/components/shadcn/badge' import { Button } from '@/components/shadcn/button' @@ -36,6 +36,7 @@ export default function OrganizationClassroomDetail() { const tc = useTranslations('common') const statusTranslations = useStatusTranslation() const locale = useLocale() + const router = useRouter() const { openModal } = useModal() const { classroomId } = useParams() @@ -96,12 +97,10 @@ export default function OrganizationClassroomDetail() {
{/* Back Button */} - - - + {/* Header Section */}
@@ -273,9 +272,7 @@ export default function OrganizationClassroomDetail() {

{student.name || student.email}

- {(student.email) && ( -

{student.email}

- )} + {student.email &&

{student.email}

}
))} diff --git a/src/features/classroom/components/upsert/CreateClassroom.tsx b/src/features/classroom/components/upsert/CreateClassroom.tsx index da852ad8d..293b39e3f 100644 --- a/src/features/classroom/components/upsert/CreateClassroom.tsx +++ b/src/features/classroom/components/upsert/CreateClassroom.tsx @@ -15,18 +15,23 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Calendar } from '@/components/shadcn/calendar' import { Popover, PopoverContent, PopoverTrigger } from '@/components/shadcn/popover' import { Button } from '@/components/shadcn/button' -import { CalendarIcon } from 'lucide-react' +import { BookOpen, CalendarIcon, Check, GraduationCap, Users } from 'lucide-react' import { format } from 'date-fns' import { cn } from '@/utils/shadcn/utils' import BackButton from '@/components/shared/button/BackButton' import GroupTableWithTeacher from '@/features/group/components/list/GroupTableWithTeacher' import { Grade } from '@/features/classroom/types/classroom.type' +import { RadioGroup, RadioGroupItem } from '@/components/shadcn/radio-group' +import { useSearchSubscriptionQuery } from '@/features/subscription/api/subscriptionApi' +import { Card, CardContent } from '@/components/shadcn/card' +import { Badge } from '@/components/shadcn/badge' +import { formatDate, formatPrice, useStatusTranslation } from '@/utils/index' +import { getStatusBadgeClass } from '@/utils/badgeColor' type ClassroomFormData = { grade: string description?: string courseId: number - organizationSubscriptionOrderId: number durationWeeks: string startDate: string endDate: string @@ -42,7 +47,6 @@ const defaultClassroomFormData: ClassroomFormData = { grade: '', description: '', courseId: 1, - organizationSubscriptionOrderId: 1, durationWeeks: '8', startDate: new Date().toISOString(), endDate: new Date(new Date().setDate(new Date().getDate() + 56)).toISOString(), @@ -52,10 +56,10 @@ const defaultClassroomFormData: ClassroomFormData = { export default function CreateClassroom() { const tc = useTranslations('common') const tt = useTranslations('toast') + const statusTranslate = useStatusTranslation() const tClassroom = useTranslations('classroom.create') const { closeModal } = useModal() - const dispatch = useAppDispatch() const router = useRouter() const locale = useLocale() @@ -68,8 +72,7 @@ export default function CreateClassroom() { }[] >([]) - const [minDate, setMinDate] = useState(undefined) - const [maxDate, setMaxDate] = useState(undefined) + const { selectedOrganizationId } = useAppSelector((state) => state.selectedOrganization) // Form states const [grade, setGrade] = useState('') @@ -77,8 +80,7 @@ export default function CreateClassroom() { const [durationWeeks, setDurationWeeks] = useState('8') const [startDate, setStartDateState] = useState(new Date()) const [endDate, setEndDate] = useState(new Date(new Date().setDate(new Date().getDate() + 56))) - - const selectedSubscriptionId = useAppSelector((state) => state.selectedOrganization.selectedSubscriptionOrderId) + const [selectedSubscriptionId, setSelectedSubscriptionId] = useState(null) const GRADE_OPTIONS = Object.values(Grade) .filter((v) => typeof v === 'number') @@ -97,6 +99,11 @@ export default function CreateClassroom() { const isCustomDuration = durationWeeks === 'custom' + const { data: subscriptions } = useSearchSubscriptionQuery({ + pageNumber: 1, + pageSize: 10, + organizationId: selectedOrganizationId! + }) const [createClassroom, { isLoading: isCreating }] = useCreateClassroomMutation() const form = useAppForm({ @@ -162,6 +169,19 @@ export default function CreateClassroom() {
+
+
+

Hướng dẫn tạo lớp học

+ +
+ Khóa học có thể thuộc nhiều gói đăng ký đang hoạt động. Vui lòng chọn một gói đăng ký trước khi tạo lớp học. + Gói đăng ký được chọn sẽ quyết định số lượng học sinh, giáo viên và quyền truy cập nội dung học tập. Sau khi + chọn gói, hãy chọn nhóm học sinh, gán giáo viên và thiết lập thời gian lớp học. Nhấn Create để hoàn tất việc + tạo lớp học. +
+
+
+ {/* Form Content */}
+
+ + +
+ {subscriptions?.data.items.map((sub) => { + const isActive = sub.status === 'Active' + const isSelected = selectedSubscriptionId === sub.id + + return ( + isActive && setSelectedSubscriptionId(sub.id)} + > + + {/* Header */} +
+

{sub.planName}

+ + {statusTranslate(sub.status.toLowerCase())} + +
+ + {/* Price */} +
+ {formatPrice(sub.netAmount)} +
+ + {/* Period */} +
+ + {tClassroom(sub.planBillingCycle.toLowerCase())} + + + {formatDate(sub.startDate, { locale })} - {formatDate(sub.endDate, { locale })} + +
+ + {/* Divider */} +
+ + {/* Stats */} +
+
+
+ + {tClassroom('students')} +
+ + {sub.currentStudentSeats}/{sub.maxStudentSeats} + +
+ +
+
+ + {tClassroom('teachers')} +
+ + {sub.currentTeacherSeats}/{sub.maxTeacherSeats} + +
+ +
+
+ + {tClassroom('curricula')} +
+ {sub.curriculumCount} +
+
+ + {/* Code */} +
+

{sub.code}

+
+ + + ) + })} +
+
+
-

{tClassroom('groupList')}

+ {/*

{tClassroom('groupList')}

*/}
diff --git a/src/features/classroom/types/classroom.type.ts b/src/features/classroom/types/classroom.type.ts index 55a576511..b0d8938f1 100644 --- a/src/features/classroom/types/classroom.type.ts +++ b/src/features/classroom/types/classroom.type.ts @@ -37,6 +37,7 @@ export type ClassroomSliceParams = { teacherId?: string status?: ClassroomStatus courseId?: number + subscriptionId?: number } & SliceQueryParams // Pending, InProgress, Completed, Deleted diff --git a/src/features/creator-3d/components/creator-workspace/CreatorWorkspace.tsx b/src/features/creator-3d/components/creator-workspace/CreatorWorkspace.tsx index 2ae31d2c8..7fc25cb4c 100644 --- a/src/features/creator-3d/components/creator-workspace/CreatorWorkspace.tsx +++ b/src/features/creator-3d/components/creator-workspace/CreatorWorkspace.tsx @@ -1,10 +1,5 @@ 'use client' -// useGLTF.preload('/models/connector_1leg.glb') -// useGLTF.preload('/models/connector_2legs.glb') -// useGLTF.preload('/models/connector_3legs.glb') -// useGLTF.preload('/models/connector_5legs.glb') - import { Canvas } from '@react-three/fiber' import { useRef, useCallback, useState, useEffect } from 'react' import { CreatorToolbar } from '@/features/creator-3d/components/creator-workspace/CreatorToolbar' @@ -20,7 +15,7 @@ import { setSelectedId } from '@/features/creator-3d/slice/creatorSceneSlice' import { syncRedo, syncUndo } from '@/features/creator-3d/slice/createSceneThunk' -import { useGLTF } from '@react-three/drei' +import { removeTargetFromAllActions } from '@/features/creator-3d/slice/workspaceTreeSlice' interface CreatorWorkspaceProps { onObjectSelect: (objectId: string | null) => void @@ -69,53 +64,55 @@ export function CreatorWorkspace({ onObjectSelect, onObjectUpdate, onObjectAdd } useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // ⌨️ Ctrl + Z → Undo (synced) - if (e.ctrlKey && e.key.toLowerCase() === 'z') { - e.preventDefault() - dispatch(syncUndo()) - return - } + // if (e.ctrlKey && e.key.toLowerCase() === 'z') { + // e.preventDefault() + // dispatch(syncUndo()) + // return + // } // ⌨️ Ctrl + Y → Redo (synced) - if (e.ctrlKey && e.key.toLowerCase() === 'y') { - e.preventDefault() - dispatch(syncRedo()) - return - } + // if (e.ctrlKey && e.key.toLowerCase() === 'y') { + // e.preventDefault() + // dispatch(syncRedo()) + // return + // } // ⌨️ Ctrl + C → Copy - if (e.ctrlKey && e.key.toLowerCase() === 'c' && selectedId) { - e.preventDefault() - const copied = instances.find((i) => i.id === selectedId) - clipboardRef.current = copied ? JSON.parse(JSON.stringify(copied)) : null - console.log('📋 Copied object:', clipboardRef.current?.id) - return - } + // if (e.ctrlKey && e.key.toLowerCase() === 'c' && selectedId) { + // e.preventDefault() + // const copied = instances.find((i) => i.id === selectedId) + // clipboardRef.current = copied ? JSON.parse(JSON.stringify(copied)) : null + // console.log('📋 Copied object:', clipboardRef.current?.id) + // return + // } // ⌨️ Ctrl + V → Paste - if (e.ctrlKey && e.key.toLowerCase() === 'v' && clipboardRef.current) { - e.preventDefault() - const base = clipboardRef.current - const newCopy: AssemblyInstance = { - ...base, - id: `${base.id}_copy_${Date.now()}`, - transform: { - ...base.transform, - position: { - x: base.transform.position.x + 1, - y: base.transform.position.y, - z: base.transform.position.z + 1 - } - } - } - dispatch(addInstance(newCopy)) - dispatch(setSelectedId(newCopy.id)) - console.log('📎 Pasted object:', newCopy.id) - return - } + // if (e.ctrlKey && e.key.toLowerCase() === 'v' && clipboardRef.current) { + // e.preventDefault() + // const base = clipboardRef.current + // const newCopy: AssemblyInstance = { + // ...base, + // id: `${base.id}_copy_${Date.now()}`, + // transform: { + // ...base.transform, + // position: { + // x: base.transform.position.x + 1, + // y: base.transform.position.y, + // z: base.transform.position.z + 1 + // } + // } + // } + // dispatch(addInstance(newCopy)) + // dispatch(setSelectedId(newCopy.id)) + // console.log('📎 Pasted object:', newCopy.id) + // return + // } if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId) { e.preventDefault() dispatch(removeInstance(selectedId)) + dispatch(removeTargetFromAllActions(selectedId)) + console.log('🗑️ Deleted object:', selectedId) return } diff --git a/src/features/creator-3d/components/creator-workspace/SceneTopRight.tsx b/src/features/creator-3d/components/creator-workspace/SceneTopRight.tsx index 18afbbd07..5c02f7cdf 100644 --- a/src/features/creator-3d/components/creator-workspace/SceneTopRight.tsx +++ b/src/features/creator-3d/components/creator-workspace/SceneTopRight.tsx @@ -1,7 +1,7 @@ import { Button } from '@/components/shadcn/button' import { clearScene } from '@/features/creator-3d/slice/creatorSceneSlice' import { setCameraStatus } from '@/features/creator-3d/slice/strawLabSlice' -import { resetActions } from '@/features/creator-3d/slice/workspaceTreeSlice' +import { resetActions, resetWorkspace } from '@/features/creator-3d/slice/workspaceTreeSlice' import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks' import { useTranslations } from 'next-intl' import { useCallback } from 'react' @@ -15,7 +15,7 @@ export default function SceneTopRight() { const handleClearScene = useCallback(() => { if (confirm('Are you sure you want to clear the entire scene? This action cannot be undone.')) { dispatch(clearScene()) - dispatch(resetActions()) + dispatch(resetWorkspace()) } }, [dispatch]) diff --git a/src/features/creator-3d/components/creator3d/Creator3D.tsx b/src/features/creator-3d/components/creator3d/Creator3D.tsx index 0f9acf1cc..b4d57f4b6 100644 --- a/src/features/creator-3d/components/creator3d/Creator3D.tsx +++ b/src/features/creator-3d/components/creator3d/Creator3D.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useCallback, useMemo, useEffect, useRef } from 'react' +import { useCallback, useMemo, useEffect, useRef } from 'react' import { ComponentPalette } from '../component-palette/ComponentPalette' import { SceneActions } from '@/features/creator-3d/components/creator3d/SceneActions' import { SceneStats } from '@/features/creator-3d/components/creator3d/SceneStats' @@ -27,8 +27,8 @@ import { useParams } from 'next/navigation' import { useUpdateEmulatorMutation } from '@/features/emulator/api/emulatorApi' import { ApiSuccessResponse } from '@/types/baseModel' import { Emulator } from '@/features/emulator/types/emulator.type' -import { buildSceneFromAssembly } from '@/features/creator-3d/hooks/buildSceneFromAssembly' import { exportGLB } from '@/features/creator-3d/hooks/exportGlb' +import { useAutosave } from '@/features/creator-3d/components/creator3d/useAutosave' type Creator3DProps = { emulatorData: ApiSuccessResponse | undefined } @@ -42,6 +42,17 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { const selectedObject = useSelectedObject() const exportAssemblyFn = useExportAssembly() const fileInputRef = useRef(null) + const { actions, activities } = useAppSelector((s) => s.workspaceTree) + const creatorState = useMemo( + () => ({ + instances, + actions, + activities + }), + [instances, actions, activities] + ) + + const autosaveKey = `creator-autosave-${workspaceId}` const [updateEmulator, { isLoading: isUpdating }] = useUpdateEmulatorMutation() const prevInstanceIds = useRef([]) @@ -120,7 +131,6 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { status: existing.status } }).unwrap() - toast.success('Lưu dữ liệu thành công.') } // console.log('Emulator creation response:', response) @@ -144,8 +154,8 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { } const existing = emulatorData.data + const data = existing.definitionJson ? JSON.parse(existing.definitionJson) : null - console.log('Importing assembly data:', data.instances.connectors) if (!data) throw new Error('Dữ liệu assembly không hợp lệ') const allInstances: AssemblyInstance[] = [] @@ -306,20 +316,6 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { // ================================ if (Array.isArray(data.activities)) { for (const activity of data.activities) { - // const fullSteps: Step[] = [] - - // if (Array.isArray(activity.steps)) { - // for (const step of activity.steps) { - // fullSteps.push({ - // actionId: step.actionId + 1111, - // title: step.title || 'Untitled Step', - // description: step.description || '', - // expectedResult: step.expectedResult || '', - // hints: step.hints || [] - // }) - // } - // } - dispatch( addActivity({ id: activity.id, @@ -335,35 +331,6 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { if (Array.isArray(activity.steps)) { for (const step of activity.steps) { dispatch(addStepToActivity({ activityId: activity.id, step })) - - // ✅ 3️⃣ Nếu step có actions thì xử lý tiếp - // if (Array.isArray(step.actions)) { - // for (const act of step.actions) { - // // Thêm action - // dispatch(addAction({ id: act.id, name: act.name, type: act.type })) - // console.log('Restoring action:', act.id, act.name, act.type) - - // // Gắn các target - // if (Array.isArray(act.targets)) { - // act.targets.forEach((targetId: string) => - // dispatch(addTargetToAction({ actionId: act.id, targetId })) - // ) - // } - - // // Restore arms nếu có - // if (act.type === 'transform_arm' && act.connectorArmTransforms) { - // Object.entries(act.connectorArmTransforms).forEach(([connectorId, arms]) => { - // dispatch( - // updateConnectorArms({ - // actionId: act.id, - // connectorId, - // arms: arms as Record - // }) - // ) - // }) - // } - // } - // } } } } @@ -531,6 +498,64 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { [dispatch] ) + const restoreFromAutosave = (data: any) => { + if (!data) return + + // Restore instances + if (Array.isArray(data.instances)) { + dispatch(setInstances(data.instances)) + } + + // Restore actions + dispatch(clearAction()) + if (Array.isArray(data.actions)) { + for (const act of data.actions) { + dispatch(addAction(act)) + + if (Array.isArray(act.targets)) { + for (const targetId of act.targets) { + dispatch(addTargetToAction({ actionId: act.id, targetId })) + } + } + + if (act.type === 'transform_arm' && act.connectorArmTransforms) { + Object.entries(act.connectorArmTransforms).forEach(([connectorId, arms]) => { + dispatch( + updateConnectorArms({ + actionId: act.id, + connectorId, + arms: arms as Record + }) + ) + }) + } + } + } + + // Restore activities + dispatch(clearActivities()) + if (Array.isArray(data.activities)) { + for (const activity of data.activities) { + dispatch( + addActivity({ + id: activity.id, + name: activity.name, + steps: [], + difficulty: activity.difficulty ?? 'medium', + description: activity.description ?? '', + estimatedTime: activity.estimatedTime ?? 10 + }) + ) + + if (Array.isArray(activity.steps)) { + for (const step of activity.steps) { + dispatch(addStepToActivity({ activityId: activity.id, step })) + } + } + } + } + } + const handleExportGLB = async () => { const assembly = exportAssemblyFn({ title: `Assembly ${workspaceId}`, @@ -578,9 +603,22 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { [dispatch] ) + const { status: cloudState, loadPromise: autosaveLoaded } = useAutosave({ + key: autosaveKey, + data: creatorState, + onLoad: restoreFromAutosave, + onSyncToServer: async () => handleSaveAssembly(), + interval: 2000, + debounce: 5000 + }) + useEffect(() => { - handleImportAssembly(workspaceId) - }, []) + autosaveLoaded.then((saved) => { + if (!saved) { + handleImportAssembly(workspaceId) + } + }) + }, [autosaveLoaded, handleImportAssembly, workspaceId]) // trong Creator3D useEffect(() => { @@ -619,7 +657,12 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { /> {/* Action Buttons */} - +
{/* Object Inspector */} diff --git a/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx b/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx index d9bc2ef50..21d2e82e2 100644 --- a/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx +++ b/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx @@ -50,7 +50,6 @@ export default function Creator3DHeader() {
- diff --git a/src/features/creator-3d/components/creator3d/ExportDialog.tsx b/src/features/creator-3d/components/creator3d/ExportDialog.tsx index b3a825624..d66395cae 100644 --- a/src/features/creator-3d/components/creator3d/ExportDialog.tsx +++ b/src/features/creator-3d/components/creator3d/ExportDialog.tsx @@ -27,7 +27,6 @@ export function UpsertEmulator({ emulationId }: UpsertEmulatorProps) { const [name, setName] = useState('') const [description, setDescription] = useState('') - console.log('description in UpsertEmulator:', description) const [visibility, setVisibility] = useState<'public' | 'private'>('public') const [thumbnailBase64, setThumbnailBase64] = useState() const [thumbnailFileName, setThumbnailFileName] = useState() diff --git a/src/features/creator-3d/components/creator3d/SceneActions.tsx b/src/features/creator-3d/components/creator3d/SceneActions.tsx index 98fbeb5e9..3dc06e678 100644 --- a/src/features/creator-3d/components/creator3d/SceneActions.tsx +++ b/src/features/creator-3d/components/creator3d/SceneActions.tsx @@ -1,12 +1,14 @@ +import { IconCloudCheck, IconCloudOff, IconLoader2, IconRefresh } from '@tabler/icons-react' import { useTranslations } from 'next-intl' interface SceneActionsProps { + cloudState: 'saved' | 'saving' onSave: () => void onImportJSON?: () => void onExportGLB?: () => void } -export function SceneActions({ onSave, onImportJSON, onExportGLB }: SceneActionsProps) { +export function SceneActions({ cloudState, onSave, onImportJSON, onExportGLB }: SceneActionsProps) { const t3d = useTranslations('creator3D.main_content') const tc = useTranslations('common') return ( @@ -14,8 +16,10 @@ export function SceneActions({ onSave, onImportJSON, onExportGLB }: SceneActions