fix(dashboard): lesson nav build error and add CI build check (#765)

* fix(dashboard): repair lesson nav build and improve locked feedback

Replace stale $isOrgStudent reference with isCourseLearnerView to fix
the Svelte build error. Disable prev/next when the adjacent item is
locked, show lock tooltips, and surface a snackbar if navigation is
blocked (including keyboard shortcuts).

Co-authored-by: Rotimi Best <[email protected]>

* ci: add package build check and AGENTS.md verification steps

Document required dashboard/API build commands for agents and add a
GitHub Actions workflow that runs those builds on PRs touching apps or
packages. Copy dashboard .env.example in CI so vite build has required
public env vars.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-16 12:34:03 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 3ef286ed39
commit ca3fdd5b6a
3 changed files with 108 additions and 11 deletions
+53
View File
@@ -0,0 +1,53 @@
name: Package build check
on:
pull_request:
paths:
- 'apps/dashboard/**'
- 'apps/api/**'
- 'packages/**'
- 'pnpm-lock.yaml'
- 'package.json'
- 'turbo.json'
- '.nvmrc'
- '.github/workflows/package-build-check.yml'
push:
branches:
- main
paths:
- 'apps/dashboard/**'
- 'apps/api/**'
- 'packages/**'
- 'pnpm-lock.yaml'
- 'package.json'
- 'turbo.json'
- '.nvmrc'
- '.github/workflows/package-build-check.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Prepare dashboard env
run: cp apps/dashboard/.env.example apps/dashboard/.env
- name: Build dashboard
run: pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build
- name: Build API
run: pnpm --filter @cio/api^... build && pnpm --filter @cio/api build
+23
View File
@@ -20,6 +20,29 @@ When a task requires factual information (API specifications, context window siz
- Commit messages must follow the Conventional Commits 1.0.0 spec: https://www.conventionalcommits.org/en/v1.0.0/
- Never include agent-provider attribution in commits or commit trailers. Do not add `Co-authored-by` lines for Cursor, Claude, Codex, or any other tool.
## Verification before commit/PR
Before committing or opening/updating a PR, run the build checks that match the areas you changed. Use Node 20 (the repo's `.nvmrc` version):
```bash
export PATH="$HOME/.nvm/versions/node/v20.19.3/bin:$PATH"
```
- **Dashboard changes** (`apps/dashboard/**`, or shared packages consumed by the dashboard):
```bash
test -f apps/dashboard/.env || cp apps/dashboard/.env.example apps/dashboard/.env
pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build
```
- **API changes** (`apps/api/**`, or shared packages consumed by the API):
```bash
pnpm --filter @cio/api^... build && pnpm --filter @cio/api build
```
Also run `pnpm format:check` (see Translation, Formatting, and Git Workflow above). Do not commit if any verification step fails.
## Naming Convention
- Use kebab-case for files (e.g. `user-profile.svelte`, `org.svelte.ts`).
@@ -7,8 +7,10 @@
import { Button } from '@cio/ui/base/button';
import * as Tooltip from '@cio/ui/base/tooltip';
import { PercentRingProgress } from '@cio/ui/custom/percent-ring-progress';
import { get } from 'svelte/store';
import { isCourseLearnerView } from '$lib/utils/store/app';
import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar';
import { getStudentContentLockTitleKey } from '$features/course/utils/content-lock-utils';
import { t } from '$lib/utils/functions/translations';
import { courseApi, lessonApi } from '$features/course/api';
import { getOrderedNavigableContent, getContentRoute } from '$features/course/utils/content';
@@ -50,16 +52,21 @@
};
});
function getNavigableLockReason(target: CourseContentItem | null) {
if (!target) return null;
const targetType = target.type === ContentType.Lesson ? ContentType.Lesson : ContentType.Exercise;
return getStudentContentLockReason(courseApi.course, target.id, targetType);
}
function goToContent(target: CourseContentItem | null) {
if (!target) return;
if ($isCourseLearnerView) {
const targetType = target.type === ContentType.Lesson ? ContentType.Lesson : ContentType.Exercise;
const lockReason = getStudentContentLockReason(courseApi.course, target.id, targetType);
const lockReason = getNavigableLockReason(target);
if (lockReason) {
return;
}
if (get(isCourseLearnerView) && lockReason) {
snackbar.error(getStudentContentLockTitleKey(lockReason));
return;
}
const courseIdResolved = courseApi.course?.id;
@@ -71,6 +78,20 @@
const isPrevDisabled = $derived(!prevNextContent.prev);
const isNextDisabled = $derived(!prevNextContent.next);
const prevNavLockReason = $derived($isCourseLearnerView ? getNavigableLockReason(prevNextContent.prev) : null);
const nextNavLockReason = $derived($isCourseLearnerView ? getNavigableLockReason(prevNextContent.next) : null);
const isPrevNavBlocked = $derived(Boolean(prevNavLockReason));
const isNextNavBlocked = $derived(Boolean(nextNavLockReason));
const prevNavTooltip = $derived(
prevNavLockReason
? $t(getStudentContentLockTitleKey(prevNavLockReason))
: $t('course.navItem.lessons.prev_shortcut')
);
const nextNavTooltip = $derived(
nextNavLockReason
? $t(getStudentContentLockTitleKey(nextNavLockReason))
: $t('course.navItem.lessons.next_shortcut')
);
const isLessonComplete = $derived.by(() => {
if (!lessonId) return false;
const lesson = lessonItems.find((l) => l.id === lessonId);
@@ -145,7 +166,7 @@
updateCourseContentCompletion(currentLessonId, isComplete);
const allComplete =
$isOrgStudent &&
get(isCourseLearnerView) &&
isComplete &&
navigableContentItems.length > 0 &&
navigableContentItems.every((item) => item.isComplete);
@@ -231,14 +252,14 @@
size="icon-sm"
variant="outline"
onclick={() => goToContent(prevNextContent.prev)}
disabled={isPrevDisabled}
disabled={isPrevDisabled || isPrevNavBlocked}
aria-label={$t('course.navItem.lessons.prev')}
>
<ChevronLeftIcon size={14} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side="bottom" sideOffset={4}>
{$t('course.navItem.lessons.prev_shortcut')}
{prevNavTooltip}
</Tooltip.Content>
</Tooltip.Root>
@@ -248,14 +269,14 @@
size="icon-sm"
variant="outline"
onclick={() => goToContent(prevNextContent.next)}
disabled={isNextDisabled}
disabled={isNextDisabled || isNextNavBlocked}
aria-label={$t('course.navItem.lessons.next')}
>
<ChevronRightIcon size={14} />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side="bottom" sideOffset={4}>
{$t('course.navItem.lessons.next_shortcut')}
{nextNavTooltip}
</Tooltip.Content>
</Tooltip.Root>
</div>