diff --git a/src/components/tutorials/TutorialList/index.tsx b/src/components/tutorials/TutorialList/index.tsx index df49664d0..c64994820 100644 --- a/src/components/tutorials/TutorialList/index.tsx +++ b/src/components/tutorials/TutorialList/index.tsx @@ -2,7 +2,7 @@ import React, { FC, useState, useEffect } from 'react' import Head from '@docusaurus/Head' import { Meta, Tutorial } from '../models' import { getSteps } from '../utils' -import TutorialGrid from '../TutorialGrid' +import TutorialTable from '../TutorialTable' import TutorialSearch from '../TutorialSearch' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@site/src/components/ui/accordion' import { @@ -182,11 +182,12 @@ const TutorialList: FC = () => { )} - {/* Main content - Center aligned with page */} -
+ {/* Main content - Aligned next to the filter sidebar, which already pads its right edge. + Top padding matches the sidebar's so the column headers line up with the search box. */} +
- {/* Tutorial Grid */} - + {/* Tutorial list */} +
diff --git a/src/components/tutorials/TutorialTable.tsx b/src/components/tutorials/TutorialTable.tsx new file mode 100644 index 000000000..ae62606b5 --- /dev/null +++ b/src/components/tutorials/TutorialTable.tsx @@ -0,0 +1,152 @@ +import React from 'react' +import Link from '@docusaurus/Link' +import { cn } from '@site/src/lib/utils' +import { Tutorial } from './models' +import { getSteps } from './utils' + +// Function to get tutorial progress from localStorage +function getTutorialProgress( + tutorialId: string, + totalSteps: number +): { completed: number; percentage: number } { + if (typeof window === 'undefined') return { completed: 0, percentage: 0 } + + const storedVisited = localStorage.getItem(`tutorial-progress-${tutorialId}`) + const visitedSteps = storedVisited + ? new Set(JSON.parse(storedVisited)) + : new Set() + const completed = visitedSteps.size + const percentage = + totalSteps > 0 ? Math.round((completed / totalSteps) * 100) : 0 + return { completed, percentage } +} + +function getFirstStepPath(tutorialId: string): string | null { + const steps = getSteps(tutorialId) + if (steps.length === 0) { + return null + } + return steps[0].path +} + +const gridColumns = + 'lg:grid lg:grid-cols-[minmax(0,3.2fr)_minmax(0,1fr)_minmax(0,1fr)_5rem] lg:gap-5 lg:items-center' + +const TutorialRow: React.FC<{ tutorial: Tutorial }> = ({ tutorial }) => { + const firstStep = getFirstStepPath(tutorial.meta.id) + const progress = getTutorialProgress(tutorial.meta.id, tutorial.steps.length) + + const rowContent = ( +
+ {/* Tutorial title and description */} +
+
+ {firstStep + ? tutorial.meta.title + : `${tutorial.meta.title} (No steps found)`} +
+

+ {tutorial.meta.description} +

+
+ + {/* Use case */} +
+ + Use case: + + {tutorial.meta.useCase} +
+ + {/* Technologies */} +
+ + Technology: + + {tutorial.meta.technologies.length > 0 ? ( + tutorial.meta.technologies.join(', ') + ) : ( + + )} +
+ + {/* Progress */} +
+ + Progress: + + + {progress.completed}/{tutorial.steps.length} + +
+
+
+
+
+ ) + + return firstStep ? ( + + {rowContent} + + ) : ( +
{rowContent}
+ ) +} + +interface TutorialTableProps { + tutorials: Tutorial[] + className?: string +} + +export const TutorialTable: React.FC = ({ + tutorials, + className, +}) => { + if (tutorials.length === 0) { + return ( +
+

+ No tutorials found matching your criteria. +

+
+ ) + } + + return ( +
+ {/* Column headers - desktop only */} +
+
Tutorial
+
Use case
+
Technology
+
Progress
+
+ +
+ {tutorials.map((tutorial) => ( + + ))} +
+
+ ) +} + +export default TutorialTable