modify publick curriculum
This commit is contained in:
+24
-1
@@ -12,6 +12,9 @@ import https from 'https'
|
|||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let splashWindow: BrowserWindow | null = null
|
let splashWindow: BrowserWindow | null = null
|
||||||
|
const MIN_SPLASH_VISIBLE_MS = 2000
|
||||||
|
let splashVisibleStartedAt = 0
|
||||||
|
let splashCloseTimer: NodeJS.Timeout | null = null
|
||||||
const terminalProcesses = new Map<string, ChildProcessWithoutNullStreams>()
|
const terminalProcesses = new Map<string, ChildProcessWithoutNullStreams>()
|
||||||
const terminalStopTimers = new Map<string, NodeJS.Timeout>()
|
const terminalStopTimers = new Map<string, NodeJS.Timeout>()
|
||||||
const intentionalTerminalStops = new Set<string>()
|
const intentionalTerminalStops = new Set<string>()
|
||||||
@@ -3176,8 +3179,16 @@ function createSplashWindow(): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
splashWindow.once('ready-to-show', () => splashWindow?.show())
|
splashVisibleStartedAt = Date.now()
|
||||||
|
splashWindow.once('ready-to-show', () => {
|
||||||
|
splashVisibleStartedAt = Date.now()
|
||||||
|
splashWindow?.show()
|
||||||
|
})
|
||||||
splashWindow.on('closed', () => {
|
splashWindow.on('closed', () => {
|
||||||
|
if (splashCloseTimer) {
|
||||||
|
clearTimeout(splashCloseTimer)
|
||||||
|
splashCloseTimer = null
|
||||||
|
}
|
||||||
splashWindow = null
|
splashWindow = null
|
||||||
})
|
})
|
||||||
splashWindow.loadFile(join(__dirname, '../renderer/splash.html')).catch((error) => {
|
splashWindow.loadFile(join(__dirname, '../renderer/splash.html')).catch((error) => {
|
||||||
@@ -3194,6 +3205,18 @@ function updateSplashStatus(message: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeSplashAndShowMain(): void {
|
function closeSplashAndShowMain(): void {
|
||||||
|
if (splashCloseTimer) return
|
||||||
|
const elapsed = splashVisibleStartedAt ? Date.now() - splashVisibleStartedAt : MIN_SPLASH_VISIBLE_MS
|
||||||
|
const remaining = Math.max(0, MIN_SPLASH_VISIBLE_MS - elapsed)
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
splashCloseTimer = setTimeout(() => {
|
||||||
|
splashCloseTimer = null
|
||||||
|
closeSplashAndShowMain()
|
||||||
|
}, remaining)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isVisible()) {
|
if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.isVisible()) {
|
||||||
splashWindow?.close()
|
splashWindow?.close()
|
||||||
splashWindow = null
|
splashWindow = null
|
||||||
|
|||||||
+48
-18
@@ -78,6 +78,8 @@ import { GitFileContextMenu, PowerActionModal, SourceSettingsSaveConfirmModal, S
|
|||||||
|
|
||||||
type IdeActivity = WorkspaceActivity
|
type IdeActivity = WorkspaceActivity
|
||||||
|
|
||||||
|
const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide'
|
||||||
|
|
||||||
function makeStudentSlug(email?: string | null): string {
|
function makeStudentSlug(email?: string | null): string {
|
||||||
return ((email ?? '').split('@')[0] || 'student').replace(/[^a-zA-Z0-9_-]+/g, '-').toLowerCase()
|
return ((email ?? '').split('@')[0] || 'student').replace(/[^a-zA-Z0-9_-]+/g, '-').toLowerCase()
|
||||||
}
|
}
|
||||||
@@ -107,6 +109,7 @@ const EMPTY_WEEK: CurriculumWeek = {
|
|||||||
github: '-',
|
github: '-',
|
||||||
detail: {
|
detail: {
|
||||||
overview: '',
|
overview: '',
|
||||||
|
learningSteps: [],
|
||||||
topicDetails: [],
|
topicDetails: [],
|
||||||
topicVideoUrls: [],
|
topicVideoUrls: [],
|
||||||
references: [],
|
references: [],
|
||||||
@@ -376,6 +379,8 @@ function LocalPcWorkspaceMockup({
|
|||||||
courseKey,
|
courseKey,
|
||||||
curriculumWeek,
|
curriculumWeek,
|
||||||
activeLearningStep,
|
activeLearningStep,
|
||||||
|
allowFutureLearningSteps,
|
||||||
|
onLearningStepSelect,
|
||||||
onSubmissionRecorded
|
onSubmissionRecorded
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -400,6 +405,8 @@ function LocalPcWorkspaceMockup({
|
|||||||
courseKey: string
|
courseKey: string
|
||||||
curriculumWeek: CurriculumWeek
|
curriculumWeek: CurriculumWeek
|
||||||
activeLearningStep: string | null
|
activeLearningStep: string | null
|
||||||
|
allowFutureLearningSteps: boolean
|
||||||
|
onLearningStepSelect?: (step: string) => void
|
||||||
onSubmissionRecorded: () => void
|
onSubmissionRecorded: () => void
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const [powerActionModalOpen, setPowerActionModalOpen] = useState(false)
|
const [powerActionModalOpen, setPowerActionModalOpen] = useState(false)
|
||||||
@@ -571,10 +578,19 @@ function LocalPcWorkspaceMockup({
|
|||||||
const timer = window.setInterval(() => setControlCountdownNow(Date.now()), 1000)
|
const timer = window.setInterval(() => setControlCountdownNow(Date.now()), 1000)
|
||||||
return () => window.clearInterval(timer)
|
return () => window.clearInterval(timer)
|
||||||
}, [controlEndsAt])
|
}, [controlEndsAt])
|
||||||
const materialItems = curriculumWeek.topics.map((topic, index) => ({
|
const materialItems = (curriculumWeek.detail.learningSteps?.length
|
||||||
|
? curriculumWeek.detail.learningSteps
|
||||||
|
: curriculumWeek.topics.map((topic, index) => ({
|
||||||
title: topic,
|
title: topic,
|
||||||
description: curriculumWeek.detail.topicDetails[index] ?? '',
|
description: curriculumWeek.detail.topicDetails[index] ?? '',
|
||||||
videoUrl: curriculumWeek.detail.topicVideoUrls?.[index] ?? ''
|
videoUrl: curriculumWeek.detail.topicVideoUrls?.[index] ?? '',
|
||||||
|
concepts: [] as string[],
|
||||||
|
}))
|
||||||
|
).map((step, index) => ({
|
||||||
|
title: step.title,
|
||||||
|
description: step.description,
|
||||||
|
videoUrl: step.videoEnabled === false ? '' : (step.videoUrl ?? ''),
|
||||||
|
concepts: step.concepts?.length ? step.concepts : [curriculumWeek.topics[index]].filter(Boolean),
|
||||||
}))
|
}))
|
||||||
const activeTopicIndex = curriculumWeek.topics.findIndex((topic) => topic === activeLearningStep)
|
const activeTopicIndex = curriculumWeek.topics.findIndex((topic) => topic === activeLearningStep)
|
||||||
const activeMaterialIndex = Math.min(
|
const activeMaterialIndex = Math.min(
|
||||||
@@ -585,7 +601,11 @@ function LocalPcWorkspaceMockup({
|
|||||||
setExpandedMaterialIndexes([activeMaterialIndex])
|
setExpandedMaterialIndexes([activeMaterialIndex])
|
||||||
}, [activeLearningStep, activeMaterialIndex, curriculumWeek.week])
|
}, [activeLearningStep, activeMaterialIndex, curriculumWeek.week])
|
||||||
const toggleMaterialItem = (index: number): void => {
|
const toggleMaterialItem = (index: number): void => {
|
||||||
if (index > activeMaterialIndex) return
|
if (!allowFutureLearningSteps && index > activeMaterialIndex) return
|
||||||
|
if (allowFutureLearningSteps) {
|
||||||
|
const nextStep = materialItems[index]?.title
|
||||||
|
if (nextStep) onLearningStepSelect?.(nextStep)
|
||||||
|
}
|
||||||
setExpandedMaterialIndexes((indexes) => (
|
setExpandedMaterialIndexes((indexes) => (
|
||||||
indexes.includes(index)
|
indexes.includes(index)
|
||||||
? indexes.filter((item) => item !== index)
|
? indexes.filter((item) => item !== index)
|
||||||
@@ -849,6 +869,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const recordLearningCompletion = async (commitCount: number, completedWeeks: number): Promise<void> => {
|
const recordLearningCompletion = async (commitCount: number, completedWeeks: number): Promise<void> => {
|
||||||
|
if (allowFutureLearningSteps) return
|
||||||
if (!accessToken) return
|
if (!accessToken) return
|
||||||
const requestAccessToken = await getFreshAccessToken()
|
const requestAccessToken = await getFreshAccessToken()
|
||||||
const recordResponse = await fetch(`${LMS_API_BASE_URL}/studio/submissions/record`, {
|
const recordResponse = await fetch(`${LMS_API_BASE_URL}/studio/submissions/record`, {
|
||||||
@@ -869,6 +890,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (allowFutureLearningSteps) return
|
||||||
if (!accessToken || !courseKey || weekNumber < 1) return
|
if (!accessToken || !courseKey || weekNumber < 1) return
|
||||||
|
|
||||||
const sessionKey = `autonomy-studio.lms.learning-session:${courseKey}:week-${weekNumber}:${platformId}`
|
const sessionKey = `autonomy-studio.lms.learning-session:${courseKey}:week-${weekNumber}:${platformId}`
|
||||||
@@ -920,7 +942,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
cancelled = true
|
cancelled = true
|
||||||
window.clearInterval(intervalId)
|
window.clearInterval(intervalId)
|
||||||
}
|
}
|
||||||
}, [accessToken, courseKey, getFreshAccessToken, platformId, weekNumber])
|
}, [accessToken, allowFutureLearningSteps, courseKey, getFreshAccessToken, platformId, weekNumber])
|
||||||
|
|
||||||
const submitAllModuleChanges = async (): Promise<void> => {
|
const submitAllModuleChanges = async (): Promise<void> => {
|
||||||
if (gitSubmitting) return
|
if (gitSubmitting) return
|
||||||
@@ -2219,6 +2241,7 @@ function LocalPcWorkspaceMockup({
|
|||||||
materialItems={materialItems}
|
materialItems={materialItems}
|
||||||
activeMaterialIndex={activeMaterialIndex}
|
activeMaterialIndex={activeMaterialIndex}
|
||||||
expandedMaterialIndexes={expandedMaterialIndexes}
|
expandedMaterialIndexes={expandedMaterialIndexes}
|
||||||
|
allowFutureMaterialSteps={allowFutureLearningSteps}
|
||||||
topics={curriculumWeek.topics}
|
topics={curriculumWeek.topics}
|
||||||
overview={curriculumWeek.detail.overview}
|
overview={curriculumWeek.detail.overview}
|
||||||
topicDetails={curriculumWeek.detail.topicDetails}
|
topicDetails={curriculumWeek.detail.topicDetails}
|
||||||
@@ -2662,18 +2685,6 @@ function LocalPcWorkspaceMockup({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function BootLoadingOverlay({ message }: { message: string }): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-[200] grid place-items-center bg-black/24 backdrop-blur-[3px]">
|
|
||||||
<section className="w-[324px] rounded-xl border border-white/14 bg-[rgba(18,24,36,0.78)] px-6 py-5 text-center shadow-[0_24px_80px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.10)] backdrop-blur-2xl">
|
|
||||||
<div className="mx-auto mb-4 h-9 w-9 animate-spin rounded-full border-2 border-white/12 border-t-[#4f8dff]" />
|
|
||||||
<p className="text-[14px] font-bold text-white">Autonomy Studio LMS</p>
|
|
||||||
<p className="mt-2 text-[12px] font-semibold text-white/58">{message}</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App(): React.JSX.Element {
|
export default function App(): React.JSX.Element {
|
||||||
const restoredStudioSessionRef = useRef<StudioRuntimeSessionState | null>(readStudioRuntimeSessionState())
|
const restoredStudioSessionRef = useRef<StudioRuntimeSessionState | null>(readStudioRuntimeSessionState())
|
||||||
const previousWeekStateRef = useRef<Record<number, CurriculumWeek['state']>>({})
|
const previousWeekStateRef = useRef<Record<number, CurriculumWeek['state']>>({})
|
||||||
@@ -2762,6 +2773,7 @@ export default function App(): React.JSX.Element {
|
|||||||
const selectedWeek = selectedWeekState ?? EMPTY_WEEK
|
const selectedWeek = selectedWeekState ?? EMPTY_WEEK
|
||||||
const workspaceWeek = workspaceWeekState ?? selectedWeekState ?? EMPTY_WEEK
|
const workspaceWeek = workspaceWeekState ?? selectedWeekState ?? EMPTY_WEEK
|
||||||
const selectedCourse = studentCourses.find((course) => course.id === selectedCourseId) ?? null
|
const selectedCourse = studentCourses.find((course) => course.id === selectedCourseId) ?? null
|
||||||
|
const publicGuideCourseActive = selectedCourse?.slug === PUBLIC_GUIDE_COURSE_SLUG
|
||||||
const studioDisabled = !selectedWeekState
|
const studioDisabled = !selectedWeekState
|
||||||
const connectedEnvironmentProfile = connectedPlatform
|
const connectedEnvironmentProfile = connectedPlatform
|
||||||
? (
|
? (
|
||||||
@@ -2777,6 +2789,13 @@ export default function App(): React.JSX.Element {
|
|||||||
&& Boolean(authSession?.user.id)
|
&& Boolean(authSession?.user.id)
|
||||||
&& session?.holderUserId === authSession?.user.id
|
&& session?.holderUserId === authSession?.user.id
|
||||||
}
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (!publicGuideCourseActive) return
|
||||||
|
const firstStep = selectedWeekState?.topics[0]
|
||||||
|
if (!firstStep) return
|
||||||
|
if (activeLearningStep && selectedWeekState.topics.includes(activeLearningStep)) return
|
||||||
|
setActiveLearningStep(firstStep)
|
||||||
|
}, [activeLearningStep, publicGuideCourseActive, selectedWeekState])
|
||||||
const curriculumSummary = curriculumWeeks.length ? `${curriculumWeeks.length}개 섹션` : '등록된 섹션 없음'
|
const curriculumSummary = curriculumWeeks.length ? `${curriculumWeeks.length}개 섹션` : '등록된 섹션 없음'
|
||||||
const showTitlebarNotice = (title: string, tone: 'info' | 'warning' | 'error' = 'info', addToList = false): void => {
|
const showTitlebarNotice = (title: string, tone: 'info' | 'warning' | 'error' = 'info', addToList = false): void => {
|
||||||
setTitlebarNotices((notices) => [
|
setTitlebarNotices((notices) => [
|
||||||
@@ -3326,6 +3345,7 @@ export default function App(): React.JSX.Element {
|
|||||||
? 'absolute inset-0'
|
? 'absolute inset-0'
|
||||||
: ['fixed bottom-0 right-0 top-[38px]', sidebarCollapsed ? 'left-[68px]' : 'left-[240px]'].join(' ')
|
: ['fixed bottom-0 right-0 top-[38px]', sidebarCollapsed ? 'left-[68px]' : 'left-[240px]'].join(' ')
|
||||||
].join(' ')
|
].join(' ')
|
||||||
|
const fullAppModalOverlayClass = 'fixed bottom-0 left-0 right-0 top-[38px] z-[80] grid place-items-center bg-black/36'
|
||||||
const canReleaseConnectedPlatform = connectedPlatform?.control?.status === 'ACTIVE'
|
const canReleaseConnectedPlatform = connectedPlatform?.control?.status === 'ACTIVE'
|
||||||
const connectedPlatformControlEndsAt = canReleaseConnectedPlatform
|
const connectedPlatformControlEndsAt = canReleaseConnectedPlatform
|
||||||
? connectedPlatform?.control?.session?.endsAt ?? null
|
? connectedPlatform?.control?.session?.endsAt ?? null
|
||||||
@@ -4044,6 +4064,8 @@ export default function App(): React.JSX.Element {
|
|||||||
courseKey={courseKey}
|
courseKey={courseKey}
|
||||||
curriculumWeek={workspaceWeek}
|
curriculumWeek={workspaceWeek}
|
||||||
activeLearningStep={activeLearningStep}
|
activeLearningStep={activeLearningStep}
|
||||||
|
allowFutureLearningSteps={publicGuideCourseActive}
|
||||||
|
onLearningStepSelect={publicGuideCourseActive ? setActiveLearningStep : undefined}
|
||||||
onSubmissionRecorded={() => {
|
onSubmissionRecorded={() => {
|
||||||
setCurriculumRefreshSignal((current) => current + 1)
|
setCurriculumRefreshSignal((current) => current + 1)
|
||||||
void loadStudentCourses({ autoSelectSingle: false })
|
void loadStudentCourses({ autoSelectSingle: false })
|
||||||
@@ -4069,8 +4091,10 @@ export default function App(): React.JSX.Element {
|
|||||||
activeLearningStep={activeLearningStep}
|
activeLearningStep={activeLearningStep}
|
||||||
studioDisabled={studioDisabled}
|
studioDisabled={studioDisabled}
|
||||||
hasMaterials={hasSelectedWeekMaterials}
|
hasMaterials={hasSelectedWeekMaterials}
|
||||||
|
publicGuideCourse={publicGuideCourseActive}
|
||||||
learningStats={learningStats}
|
learningStats={learningStats}
|
||||||
onSelectWeek={setSelectedWeekState}
|
onSelectWeek={setSelectedWeekState}
|
||||||
|
onSelectLearningStep={publicGuideCourseActive ? setActiveLearningStep : undefined}
|
||||||
onStartStudio={requestStudioStart}
|
onStartStudio={requestStudioStart}
|
||||||
onOpenMaterials={() => {
|
onOpenMaterials={() => {
|
||||||
if (hasSelectedWeekMaterials) setMaterialModalOpen(true)
|
if (hasSelectedWeekMaterials) setMaterialModalOpen(true)
|
||||||
@@ -4228,7 +4252,7 @@ export default function App(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
{sessionConfirmAction && (
|
{sessionConfirmAction && (
|
||||||
<SessionConfirmModal
|
<SessionConfirmModal
|
||||||
overlayClassName={workspaceOnlyModalOverlayClass}
|
overlayClassName={fullAppModalOverlayClass}
|
||||||
dark={appModalDark}
|
dark={appModalDark}
|
||||||
action={sessionConfirmAction}
|
action={sessionConfirmAction}
|
||||||
platformName={activePlatformName}
|
platformName={activePlatformName}
|
||||||
@@ -4255,7 +4279,13 @@ export default function App(): React.JSX.Element {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{bootLoadingVisible && <BootLoadingOverlay message={bootLoadingMessage} />}
|
{bootLoadingVisible && (
|
||||||
|
<PlatformLoadingOverlay
|
||||||
|
title="Autonomy Studio LMS"
|
||||||
|
description={bootLoadingMessage}
|
||||||
|
className="fixed inset-0 z-[200]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ export function ExpandableMarkdownMessage({
|
|||||||
}, [content, defaultCollapsed])
|
}, [content, defaultCollapsed])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (collapseOnSignal) setExpanded(false)
|
if (collapseOnSignal) {
|
||||||
|
setExpanded(false)
|
||||||
|
}
|
||||||
}, [collapseOnSignal, collapseSignal])
|
}, [collapseOnSignal, collapseSignal])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -195,7 +197,9 @@ export function ExpandablePlainMessage({
|
|||||||
}, [content])
|
}, [content])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (collapseOnSignal) setExpanded(false)
|
if (collapseOnSignal) {
|
||||||
|
setExpanded(false)
|
||||||
|
}
|
||||||
}, [collapseOnSignal, collapseSignal])
|
}, [collapseOnSignal, collapseSignal])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ type CurriculumDetailPanelProps = {
|
|||||||
week: CurriculumDetailWeek
|
week: CurriculumDetailWeek
|
||||||
selected: boolean
|
selected: boolean
|
||||||
activeLearningStep: string | null
|
activeLearningStep: string | null
|
||||||
|
allowLearningStepSelection?: boolean
|
||||||
|
onSelectLearningStep?: (step: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CurriculumDetailPanel({
|
export default function CurriculumDetailPanel({
|
||||||
week,
|
week,
|
||||||
selected,
|
selected,
|
||||||
activeLearningStep
|
activeLearningStep,
|
||||||
|
allowLearningStepSelection = false,
|
||||||
|
onSelectLearningStep
|
||||||
}: CurriculumDetailPanelProps): React.JSX.Element {
|
}: CurriculumDetailPanelProps): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div data-help="curriculum-detail" className="min-w-0 px-7 pb-7 pt-5">
|
<div data-help="curriculum-detail" className="min-w-0 px-7 pb-7 pt-5">
|
||||||
@@ -42,18 +46,28 @@ export default function CurriculumDetailPanel({
|
|||||||
<div className="mt-4 grid gap-3">
|
<div className="mt-4 grid gap-3">
|
||||||
{week.topics.map((topic, index) => {
|
{week.topics.map((topic, index) => {
|
||||||
const active = topic === activeLearningStep
|
const active = topic === activeLearningStep
|
||||||
|
const selectable = allowLearningStepSelection && Boolean(onSelectLearningStep)
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
key={topic}
|
key={topic}
|
||||||
|
role={selectable ? 'button' : undefined}
|
||||||
|
tabIndex={selectable ? 0 : undefined}
|
||||||
|
onClick={selectable ? () => onSelectLearningStep?.(topic) : undefined}
|
||||||
|
onKeyDown={selectable ? (event) => {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||||
|
event.preventDefault()
|
||||||
|
onSelectLearningStep?.(topic)
|
||||||
|
} : undefined}
|
||||||
className={[
|
className={[
|
||||||
'rounded-lg border p-4 shadow-[0_10px_22px_rgba(20,27,44,0.04)]',
|
'rounded-lg border p-4 shadow-[0_10px_22px_rgba(20,27,44,0.04)] transition',
|
||||||
|
selectable ? 'cursor-pointer hover:border-[#b7a8ff] hover:bg-[#fbf9ff]' : '',
|
||||||
active ? 'border-[#714ef6] bg-[#f7f3ff]' : 'border-[#dde2ec] bg-white'
|
active ? 'border-[#714ef6] bg-[#f7f3ff]' : 'border-[#dde2ec] bg-white'
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<span className={`grid h-7 w-7 shrink-0 place-items-center rounded-md text-[12px] font-bold ${active ? 'bg-[#714ef6] text-white' : 'bg-[#ede9ff] text-[#714ef6]'}`}>{index + 1}</span>
|
<span className={`grid h-7 w-7 shrink-0 place-items-center rounded-md text-[12px] font-bold ${active ? 'bg-[#714ef6] text-white' : 'bg-[#ede9ff] text-[#714ef6]'}`}>{index + 1}</span>
|
||||||
<h3 className="min-w-0 text-[15px] font-bold text-[#111520]">{topic}</h3>
|
<h3 className="min-w-0 text-[15px] font-bold text-[#111520]">{topic}</h3>
|
||||||
{active ? <span className="rounded bg-[#ede9ff] px-2 py-1 text-[11px] font-bold text-[#714ef6]">현재</span> : null}
|
{active ? <span className="rounded bg-[#ede9ff] px-2 py-1 text-[11px] font-bold text-[#714ef6]">진행중</span> : null}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 pl-[38px] text-[14px] font-medium leading-[1.5] text-[#4f596b]">
|
<p className="mt-1 pl-[38px] text-[14px] font-medium leading-[1.5] text-[#4f596b]">
|
||||||
{week.detail.topicDetails[index]}
|
{week.detail.topicDetails[index]}
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ type CurriculumHomeScreenProps<Week extends CurriculumHomeWeek> = {
|
|||||||
activeLearningStep: string | null
|
activeLearningStep: string | null
|
||||||
studioDisabled: boolean
|
studioDisabled: boolean
|
||||||
hasMaterials: boolean
|
hasMaterials: boolean
|
||||||
|
publicGuideCourse: boolean
|
||||||
learningStats: LearningStatItem[]
|
learningStats: LearningStatItem[]
|
||||||
onSelectWeek: (week: Week) => void
|
onSelectWeek: (week: Week) => void
|
||||||
|
onSelectLearningStep?: (step: string) => void
|
||||||
onStartStudio: () => void
|
onStartStudio: () => void
|
||||||
onOpenMaterials: () => void
|
onOpenMaterials: () => void
|
||||||
}
|
}
|
||||||
@@ -47,8 +49,10 @@ export default function CurriculumHomeScreen<Week extends CurriculumHomeWeek>({
|
|||||||
activeLearningStep,
|
activeLearningStep,
|
||||||
studioDisabled,
|
studioDisabled,
|
||||||
hasMaterials,
|
hasMaterials,
|
||||||
|
publicGuideCourse,
|
||||||
learningStats,
|
learningStats,
|
||||||
onSelectWeek,
|
onSelectWeek,
|
||||||
|
onSelectLearningStep,
|
||||||
onStartStudio,
|
onStartStudio,
|
||||||
onOpenMaterials
|
onOpenMaterials
|
||||||
}: CurriculumHomeScreenProps<Week>): React.JSX.Element {
|
}: CurriculumHomeScreenProps<Week>): React.JSX.Element {
|
||||||
@@ -91,6 +95,8 @@ export default function CurriculumHomeScreen<Week extends CurriculumHomeWeek>({
|
|||||||
week={selectedWeek}
|
week={selectedWeek}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
activeLearningStep={activeLearningStep}
|
activeLearningStep={activeLearningStep}
|
||||||
|
allowLearningStepSelection={publicGuideCourse}
|
||||||
|
onSelectLearningStep={onSelectLearningStep}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +109,7 @@ export default function CurriculumHomeScreen<Week extends CurriculumHomeWeek>({
|
|||||||
onStartStudio={onStartStudio}
|
onStartStudio={onStartStudio}
|
||||||
onOpenMaterials={onOpenMaterials}
|
onOpenMaterials={onOpenMaterials}
|
||||||
/>
|
/>
|
||||||
<LearningStatsCard stats={learningStats} />
|
<LearningStatsCard guideMode={publicGuideCourse} stats={learningStats} />
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,21 +17,28 @@ const STAT_ICONS: Record<string, LucideIcon> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LearningStatsCardProps = {
|
type LearningStatsCardProps = {
|
||||||
|
guideMode?: boolean
|
||||||
stats: LearningStatItem[]
|
stats: LearningStatItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LearningStatsCard({ stats }: LearningStatsCardProps): React.JSX.Element {
|
export default function LearningStatsCard({ guideMode = false, stats }: LearningStatsCardProps): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-lg border border-[#d6dce8] bg-white/95 p-4 shadow-[0_16px_36px_rgba(20,27,44,0.10)]">
|
<section className="rounded-lg border border-[#d6dce8] bg-white/95 p-4 shadow-[0_16px_36px_rgba(20,27,44,0.10)]">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<strong className="block font-bold">내 학습 현황</strong>
|
<strong className="block font-bold">{guideMode ? '체험 강좌' : '내 학습 현황'}</strong>
|
||||||
<span className="mt-1 block text-[12px] font-medium text-[#5f6878]">전체 진행 상황 및 통계</span>
|
<span className="mt-1 block text-[12px] font-medium text-[#5f6878]">
|
||||||
|
{guideMode ? '배정 전 사용법 안내 과정' : '전체 진행 상황 및 통계'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 overflow-hidden rounded-lg border border-[#dfe3ed] bg-[#fbfcff] shadow-[0_10px_24px_rgba(20,27,44,0.06)]">
|
<div className="mt-4 overflow-hidden rounded-lg border border-[#dfe3ed] bg-[#fbfcff] shadow-[0_10px_24px_rgba(20,27,44,0.06)]">
|
||||||
{stats.length ? (
|
{guideMode ? (
|
||||||
|
<div className="px-4 py-5 text-[12px] font-medium leading-relaxed text-[#5f6878]">
|
||||||
|
이 강좌는 Autonomy Studio LMS 사용법을 확인하기 위한 공통 안내입니다. 실제 학습 시간, 커밋 수, 제출 현황은 강사가 배정한 강좌에서부터 집계됩니다.
|
||||||
|
</div>
|
||||||
|
) : stats.length ? (
|
||||||
stats.map(({ value, label, iconKey }, index) => {
|
stats.map(({ value, label, iconKey }, index) => {
|
||||||
const Icon = STAT_ICONS[iconKey] ?? Gauge
|
const Icon = STAT_ICONS[iconKey] ?? Gauge
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export type DownloadResource = {
|
|||||||
|
|
||||||
export type LearningStat = LearningStatItem
|
export type LearningStat = LearningStatItem
|
||||||
|
|
||||||
|
export type LearningStep = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
videoEnabled?: boolean
|
||||||
|
videoUrl?: string
|
||||||
|
concepts?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export type CurriculumWeek = {
|
export type CurriculumWeek = {
|
||||||
week: number
|
week: number
|
||||||
title: string
|
title: string
|
||||||
@@ -34,6 +42,7 @@ export type CurriculumWeek = {
|
|||||||
github: string
|
github: string
|
||||||
detail: {
|
detail: {
|
||||||
overview: string
|
overview: string
|
||||||
|
learningSteps?: LearningStep[]
|
||||||
topicDetails: string[]
|
topicDetails: string[]
|
||||||
topicVideoUrls?: string[]
|
topicVideoUrls?: string[]
|
||||||
practiceContents?: Array<{
|
practiceContents?: Array<{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type WorkspaceGuideMaterialItem = {
|
|||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
videoUrl: string
|
videoUrl: string
|
||||||
|
concepts?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceGuideResource = {
|
type WorkspaceGuideResource = {
|
||||||
@@ -29,6 +30,7 @@ type WorkspaceGuidePanelProps<Resource extends WorkspaceGuideResource> = {
|
|||||||
materialItems: WorkspaceGuideMaterialItem[]
|
materialItems: WorkspaceGuideMaterialItem[]
|
||||||
activeMaterialIndex: number
|
activeMaterialIndex: number
|
||||||
expandedMaterialIndexes: number[]
|
expandedMaterialIndexes: number[]
|
||||||
|
allowFutureMaterialSteps?: boolean
|
||||||
topics: string[]
|
topics: string[]
|
||||||
overview: string
|
overview: string
|
||||||
topicDetails: string[]
|
topicDetails: string[]
|
||||||
@@ -81,6 +83,7 @@ export default function WorkspaceGuidePanel<Resource extends WorkspaceGuideResou
|
|||||||
materialItems,
|
materialItems,
|
||||||
activeMaterialIndex,
|
activeMaterialIndex,
|
||||||
expandedMaterialIndexes,
|
expandedMaterialIndexes,
|
||||||
|
allowFutureMaterialSteps = false,
|
||||||
topics,
|
topics,
|
||||||
overview,
|
overview,
|
||||||
topicDetails,
|
topicDetails,
|
||||||
@@ -142,7 +145,7 @@ export default function WorkspaceGuidePanel<Resource extends WorkspaceGuideResou
|
|||||||
const active = index === activeMaterialIndex
|
const active = index === activeMaterialIndex
|
||||||
const previous = index < activeMaterialIndex
|
const previous = index < activeMaterialIndex
|
||||||
const expanded = expandedMaterialIndexes.includes(index)
|
const expanded = expandedMaterialIndexes.includes(index)
|
||||||
const canToggle = previous || active
|
const canToggle = allowFutureMaterialSteps || previous || active
|
||||||
const youtubeEmbedUrl = getYoutubeEmbedUrl(item.videoUrl)
|
const youtubeEmbedUrl = getYoutubeEmbedUrl(item.videoUrl)
|
||||||
const youtubeVideoId = getYoutubeVideoId(item.videoUrl)
|
const youtubeVideoId = getYoutubeVideoId(item.videoUrl)
|
||||||
|
|
||||||
@@ -171,11 +174,11 @@ export default function WorkspaceGuidePanel<Resource extends WorkspaceGuideResou
|
|||||||
<div className={['flex gap-2.5', expanded ? 'items-start' : 'items-center'].join(' ')}>
|
<div className={['flex gap-2.5', expanded ? 'items-start' : 'items-center'].join(' ')}>
|
||||||
<span className={[
|
<span className={[
|
||||||
'grid h-6 w-6 shrink-0 place-items-center rounded-md text-[11px] font-bold',
|
'grid h-6 w-6 shrink-0 place-items-center rounded-md text-[11px] font-bold',
|
||||||
active ? 'bg-[#2563eb]/18 text-[#9dbdff]' : previous ? dark ? 'bg-white/12 text-white/70' : 'bg-white text-[#4f596b]' : dark ? 'bg-white/8 text-white/38' : 'bg-white text-[#9aa3b2]'
|
active ? 'bg-[#2563eb]/18 text-[#9dbdff]' : (previous || allowFutureMaterialSteps) ? dark ? 'bg-white/12 text-white/70' : 'bg-white text-[#4f596b]' : dark ? 'bg-white/8 text-white/38' : 'bg-white text-[#9aa3b2]'
|
||||||
].join(' ')}>{index + 1}</span>
|
].join(' ')}>{index + 1}</span>
|
||||||
<div className={['min-w-0 flex-1', expanded ? 'pt-0.5' : ''].join(' ')}>
|
<div className={['min-w-0 flex-1', expanded ? 'pt-0.5' : ''].join(' ')}>
|
||||||
<div className="flex min-w-0 items-center justify-between gap-2">
|
<div className="flex min-w-0 items-center justify-between gap-2">
|
||||||
<strong className={['block min-w-0 truncate text-[13px] font-bold', active || previous ? textStrong : textDim].join(' ')}>{item.title}</strong>
|
<strong className={['block min-w-0 truncate text-[13px] font-bold', active || previous || allowFutureMaterialSteps ? textStrong : textDim].join(' ')}>{item.title}</strong>
|
||||||
<span className="flex shrink-0 items-center gap-1.5">
|
<span className="flex shrink-0 items-center gap-1.5">
|
||||||
{active && (
|
{active && (
|
||||||
<span className="rounded border border-[#60a5fa]/24 bg-[#2563eb]/16 px-2 py-0.5 text-[10px] font-bold text-[#bfdbfe]">
|
<span className="rounded border border-[#60a5fa]/24 bg-[#2563eb]/16 px-2 py-0.5 text-[10px] font-bold text-[#bfdbfe]">
|
||||||
@@ -232,10 +235,10 @@ export default function WorkspaceGuidePanel<Resource extends WorkspaceGuideResou
|
|||||||
<section className={['border-b pb-2.5', borderSoft].join(' ')}>
|
<section className={['border-b pb-2.5', borderSoft].join(' ')}>
|
||||||
<span className={['block text-[11px] font-bold', textMid].join(' ')}>핵심 개념</span>
|
<span className={['block text-[11px] font-bold', textMid].join(' ')}>핵심 개념</span>
|
||||||
<div className="mt-2 grid gap-1.5">
|
<div className="mt-2 grid gap-1.5">
|
||||||
{topics.map((topic) => (
|
{(item.concepts?.length ? item.concepts : [topics[index]].filter(Boolean)).map((concept) => (
|
||||||
<div key={topic} className="flex items-center gap-2">
|
<div key={concept} className="flex items-center gap-2">
|
||||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[#2563eb]" />
|
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[#2563eb]" />
|
||||||
<span className={['text-[12px] font-medium leading-snug', textDim].join(' ')}>{topic}</span>
|
<span className={['text-[12px] font-medium leading-snug', textDim].join(' ')}>{concept}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user