feat: seed learning resources and extend UI#25
Conversation
- Add unique constraint on CompetencyRelationship to prevent duplicates - Add database indexes on CompetencyResourceLink foreign keys for performance - Make userId optional in CompetencyResourceLink with SetNull on delete - Add error logging and visibility in migration system - Move database docs to root as DATABASE.md - Sync package-lock.json with package.json (TypeScript 5.9.3) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com>
Co-authored-by: Maximilian Anzinger <44003963+MaximilianAnzinger@users.noreply.github.com>
…om:ls1intum/memo into feature/setup-domain-core-db-communication
…ate' into feature/ui-db-integration
There was a problem hiding this comment.
Pull request overview
This pull request adds initial seeding capabilities for learning resources and significantly extends the UI with new service layers, repository patterns, and enhanced page designs. The changes implement a clean layered architecture following domain-driven design principles and introduce a comprehensive mapping session interface for competency relationships.
Key Changes:
- Introduces database seeding infrastructure with demo user and sample competencies/learning resources
- Implements complete service and repository layers for User, Competency, LearningResource, CompetencyRelationship, and CompetencyResourceLink entities
- Redesigns the session page with interactive competency mapping, keyboard shortcuts, and live relationship preview
- Updates the home, about, and navigation components with modern glassmorphic styling and improved UX
Reviewed changes
Copilot reviewed 41 out of 44 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| prisma/seed.js | Adds seeding script for demo user, 5 competencies, and 5 learning resources with environment validation |
| package.json | Configures Prisma seed command and updates Next.js to 15.5.9 |
| lib/services/*.ts | Implements service layer with business logic for all domain entities |
| lib/repositories/dc_repo.ts | Implements Prisma-based repositories as the sole database access layer |
| lib/repositories/dc_interface.ts | Defines repository contracts for dependency injection |
| lib/domain/domain_core.ts | Defines shared domain models and input types |
| app/actions/*.ts | Implements server actions exposing CRUD operations to client components |
| app/session/page.tsx | Complete rewrite with interactive competency mapping, keyboard shortcuts, relationship type selection, and undo functionality |
| components/navbar.tsx | Updates with centered sliding navigation indicator and improved glassmorphic design |
| components/footer.tsx | Adds new footer component with links and copyright |
| components/ui/tooltip.tsx | Implements custom tooltip component with portal rendering and positioning |
| components/ui/kbd.tsx | Adds keyboard shortcut display component |
| app/page.tsx | Updates home page with session preview image and restructured problem statement section |
| app/about/page.tsx | Redesigns about page with team gallery, course offerings tabs, and FAQ section |
| app/layout.tsx | Wraps all pages with global gradient background and footer |
| app/globals.css | Adds scrollbar styling utilities |
| Dockerfile | Adds prisma generate step to build process |
| DOMAIN_CORE.md | Documents the layered architecture patterns and usage examples |
| DATABASE.md | Adds documentation for seed command |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return []; | ||
| } | ||
|
|
||
| const shuffled = [...all].sort(() => Math.random() - 0.5); |
There was a problem hiding this comment.
The shuffling algorithm using Math.random() - 0.5 is not a proper shuffle implementation. It has known biases and doesn't provide a uniform distribution. Use the Fisher-Yates shuffle algorithm instead for proper randomization.
| <div className="absolute inset-0 bg-white" aria-hidden /> | ||
| <div | ||
| className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,1)_20%,rgba(255,255,255,1)_80%,rgba(255,255,255,0)_100%)]" | ||
| aria-hidden |
There was a problem hiding this comment.
The variable 'aria-hidden' should be a boolean attribute 'aria-hidden={true}' or 'aria-hidden="true"' instead of just 'aria-hidden' without a value. While browsers may handle this, it's not semantically correct.
| <div className="absolute inset-0 bg-white" aria-hidden /> | |
| <div | |
| className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,1)_20%,rgba(255,255,255,1)_80%,rgba(255,255,255,0)_100%)]" | |
| aria-hidden | |
| <div className="absolute inset-0 bg-white" aria-hidden="true" /> | |
| <div | |
| className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,1)_20%,rgba(255,255,255,1)_80%,rgba(255,255,255,0)_100%)]" | |
| aria-hidden="true" |
| <div className="absolute inset-0 bg-white" aria-hidden /> | ||
| <div | ||
| className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,1)_20%,rgba(255,255,255,1)_80%,rgba(255,255,255,0)_100%)]" | ||
| aria-hidden |
There was a problem hiding this comment.
The variable 'aria-hidden' should be a boolean attribute 'aria-hidden={true}' or 'aria-hidden="true"' instead of just 'aria-hidden' without a value. While browsers may handle this, it's not semantically correct.
| <div className="absolute inset-0 bg-white" aria-hidden /> | |
| <div | |
| className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,1)_20%,rgba(255,255,255,1)_80%,rgba(255,255,255,0)_100%)]" | |
| aria-hidden | |
| <div className="absolute inset-0 bg-white" aria-hidden="true" /> | |
| <div | |
| className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_bottom,rgba(255,255,255,0)_0%,rgba(255,255,255,1)_20%,rgba(255,255,255,1)_80%,rgba(255,255,255,0)_100%)]" | |
| aria-hidden="true" |
| <div className="flex flex-wrap gap-2 flex-shrink-0"> | ||
| {/* Placeholder badges; replace with competency metadata once available */} | ||
| <Tooltip side="top"> | ||
| <TooltipTrigger asChild> | ||
| <Badge | ||
| tabIndex={0} | ||
| className="bg-slate-100 text-slate-700 border border-slate-300 cursor-help hover:bg-slate-200 transition-colors" | ||
| > | ||
| Apply | ||
| <Info className="h-3 w-3 ml-1.5" /> | ||
| </Badge> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p> | ||
| Bloom's Taxonomy categorizes learning | ||
| objectives by cognitive level. | ||
| </p> | ||
| <p className="mt-1"> | ||
| → Apply: use knowledge in practice (implement, | ||
| execute, solve). | ||
| </p> | ||
| <p className="mt-1 text-[11px] text-slate-200"> | ||
| Levels: Remember • Understand • Apply • Analyze | ||
| • Evaluate • Create | ||
| </p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| <Badge className="bg-slate-100 text-slate-700 border border-slate-300"> | ||
| Control Flow | ||
| </Badge> | ||
| </div> | ||
| <div className="flex-1 flex flex-col min-h-0 space-y-2"> | ||
| <CardTitle className="text-lg font-bold text-slate-900 flex-shrink-0"> | ||
| {competencies[0]!.title} | ||
| </CardTitle> | ||
| <div | ||
| className="flex-1 overflow-y-scroll pr-2 scrollbar-thin" | ||
| style={SCROLLBAR_STYLES} | ||
| > | ||
| <CardDescription className="text-base leading-relaxed text-slate-600"> | ||
| {competencies[0]!.description} | ||
| </CardDescription> | ||
| </div> | ||
| </div> | ||
| </CardHeader> | ||
| </Card> | ||
| )} | ||
|
|
||
| {/* Arrow Connection with Swap Button */} | ||
| <div className="flex flex-col items-center justify-center z-10 px-4 gap-3"> | ||
| <div className="flex items-center gap-2"> | ||
| <div className="h-0.5 w-12 bg-gradient-to-r from-[#0a4da2] to-[#4263eb]" /> | ||
| <div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#0a4da2] via-[#4263eb] to-[#9775fa] shadow-xl ring-3 ring-white"> | ||
| <ArrowRight | ||
| className="h-6 w-6 text-white" | ||
| aria-hidden="true" | ||
| /> | ||
| <Label | ||
| htmlFor={value} | ||
| className="text-sm font-normal text-slate-800" | ||
| > | ||
| {label} | ||
| </Label> | ||
| </div> | ||
| ))} | ||
| </RadioGroup> | ||
| </CardContent> | ||
| </Card> | ||
| </div> | ||
| <div className="h-0.5 w-12 bg-gradient-to-l from-[#9775fa] to-[#5538d1]" /> | ||
| </div> | ||
| {competencies && competencies.length >= 2 && ( | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={e => { | ||
| if (competencies && competencies.length >= 2) { | ||
| setCompetencies([ | ||
| competencies[1]!, | ||
| competencies[0]!, | ||
| ]); | ||
| } | ||
| // Remove focus after click | ||
| (e.currentTarget as HTMLButtonElement).blur(); | ||
| }} | ||
| className="text-slate-600 hover:text-slate-900 hover:bg-slate-100 focus:outline-none" | ||
| > | ||
| <ArrowLeftRight className="h-4 w-4 mr-1.5" /> | ||
| Swap Direction | ||
| </Button> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="col-span-1"> | ||
| <CompetencyCard {...competencies[1]} /> | ||
| </div> | ||
| </div> | ||
| {/* Destination Competency */} | ||
| {isLoading || !competencies || !competencies[1] ? ( | ||
| <Card className="border border-slate-200 bg-slate-50/50"> | ||
| <CardHeader> | ||
| <CardTitle className="text-slate-400"> | ||
| Loading... | ||
| </CardTitle> | ||
| </CardHeader> | ||
| </Card> | ||
| ) : ( | ||
| <Card className="relative flex h-[300px] flex-col border-2 border-[#9775fa]/30 bg-gradient-to-br from-purple-50/80 to-white shadow-lg transition-all hover:border-[#9775fa]/50 overflow-hidden"> | ||
| <div className="absolute left-0 top-0 h-full w-1.5 bg-gradient-to-b from-[#9775fa] to-[#5538d1] rounded-r-full" /> | ||
| <CardHeader className="flex h-full flex-col space-y-3 pb-3 pl-5 overflow-visible"> | ||
| <div className="flex-shrink-0"> | ||
| <Badge className="w-fit bg-[#7c3aed] text-white border-[#7c3aed] font-semibold text-xs px-3 py-1.5"> | ||
| Competency | ||
| </Badge> | ||
| </div> | ||
| <div className="flex flex-wrap gap-2 flex-shrink-0"> | ||
| {/* Placeholder badges; replace with competency metadata once available */} | ||
| <Tooltip side="top"> | ||
| <TooltipTrigger asChild> | ||
| <Badge | ||
| tabIndex={0} | ||
| className="bg-slate-100 text-slate-700 border border-slate-300 cursor-help hover:bg-slate-200 transition-colors" | ||
| > | ||
| Apply | ||
| <Info className="h-3 w-3 ml-1.5" /> | ||
| </Badge> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p> | ||
| Bloom's Taxonomy categorizes learning | ||
| objectives by cognitive level. | ||
| </p> | ||
| <p className="mt-1"> | ||
| → Apply: use knowledge in practice (implement, | ||
| execute, solve). | ||
| </p> | ||
| <p className="mt-1 text-[11px] text-slate-200"> | ||
| Levels: Remember • Understand • Apply • Analyze | ||
| • Evaluate • Create | ||
| </p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| <Badge className="bg-slate-100 text-slate-700 border border-slate-300"> | ||
| Programming Fundamentals | ||
| </Badge> |
There was a problem hiding this comment.
These placeholder badge values ('Apply', 'Control Flow', 'Programming Fundamentals') are hardcoded and repeated across both competency cards. They don't reflect the actual competency data and should either be removed, replaced with actual competency metadata, or clearly marked as placeholder/mock data.
| isMountedRef.current = false; | ||
| window.removeEventListener('keydown', handleKeyDown); |
There was a problem hiding this comment.
The isMountedRef pattern is used to prevent state updates after unmounting, but the cleanup function sets isMountedRef.current = false before the event listener is removed. This could cause a race condition where an event fires during cleanup but after the mounted flag is set to false. Consider removing the event listener first, then setting the mounted flag.
| isMountedRef.current = false; | |
| window.removeEventListener('keydown', handleKeyDown); | |
| window.removeEventListener('keydown', handleKeyDown); | |
| isMountedRef.current = false; |
| results across recommender systems hard to compare and trust. | ||
| </p> | ||
| </div> | ||
| <button className="w-full max-w-xl rounded-2xl border border-[#d9e6ff] bg-[#e9f1ff] px-6 py-6 text-lg font-semibold text-[#0a4da2] shadow-[0_22px_60px_-36px_rgba(7,30,84,0.35)] transition hover:-translate-y-0.5 hover:shadow-[0_26px_70px_-40px_rgba(7,30,84,0.45)]"> |
There was a problem hiding this comment.
The 'Visualization' button has no onClick handler or proper interactive functionality. If this is a placeholder, consider adding disabled={true} or aria-disabled="true" to indicate it's not yet functional, or add a comment explaining its future purpose.
| <button className="w-full max-w-xl rounded-2xl border border-[#d9e6ff] bg-[#e9f1ff] px-6 py-6 text-lg font-semibold text-[#0a4da2] shadow-[0_22px_60px_-36px_rgba(7,30,84,0.35)] transition hover:-translate-y-0.5 hover:shadow-[0_26px_70px_-40px_rgba(7,30,84,0.45)]"> | |
| <button | |
| type="button" | |
| disabled | |
| aria-disabled="true" | |
| className="w-full max-w-xl rounded-2xl border border-[#d9e6ff] bg-[#e9f1ff] px-6 py-6 text-lg font-semibold text-[#0a4da2] shadow-[0_22px_60px_-36px_rgba(7,30,84,0.35)] transition hover:-translate-y-0.5 hover:shadow-[0_26px_70px_-40px_rgba(7,30,84,0.45)]" | |
| > |
| // Remove focus after click | ||
| (e.currentTarget as HTMLButtonElement).blur(); |
There was a problem hiding this comment.
The Swap Direction button uses blur() to remove focus after click, which can negatively impact keyboard navigation and accessibility. Users navigating with keyboard will lose their position. Consider letting the browser handle focus naturally or implementing proper focus management.
| // Remove focus after click | |
| (e.currentTarget as HTMLButtonElement).blur(); |
| {activeIndex >= 0 && ( | ||
| <div | ||
| className="absolute inset-y-1 rounded-full bg-gradient-to-r from-[#0a4da2] to-[#7c6cff] shadow-md transition-all duration-300 ease-out" | ||
| style={{ | ||
| width: 'calc(50% - 1rem)', | ||
| left: activeIndex === 1 ? 'calc(50% + 0.5rem)' : '0.5rem', | ||
| }} | ||
| aria-hidden="true" | ||
| /> | ||
| )} |
There was a problem hiding this comment.
The navbar uses a complex centered sliding navigation pattern with absolute positioning. The activeIndex calculation assumes only 2 navigation items. If additional navigation items are added in the future, the calculation on line 78 (activeIndex === 1 ? 'calc(50% + 0.5rem)' : '0.5rem') will break and needs to be more dynamic to support n items.
| <button className="inline-flex w-fit items-center gap-2 rounded-full bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:-translate-y-0.5 hover:bg-black"> | ||
| Explore teaching | ||
| <span className="inline-block text-base">→</span> | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="lg:col-span-5 flex flex-col gap-4"> | ||
| <div className="relative aspect-[4/3] overflow-hidden rounded-3xl border border-slate-200 bg-slate-100 shadow-md flex items-center justify-center text-slate-400 text-sm font-semibold"> | ||
| Visual placeholder | ||
| </div> | ||
| <div className="relative overflow-hidden rounded-3xl border border-slate-900 bg-slate-900 p-6 text-white shadow-xl"> | ||
| <div className="flex items-start justify-between gap-4"> | ||
| <div className="space-y-3"> | ||
| <div className="text-xl font-semibold leading-tight"> | ||
| Meet our lecturers | ||
| </div> | ||
| <p className="text-sm text-white/70"> | ||
| Coaches, researchers, and industry mentors guiding student | ||
| teams on architecture, AI, and continuous delivery across | ||
| TUM courses. | ||
| </p> | ||
| <button className="inline-flex items-center gap-2 rounded-full border border-white/25 bg-white/10 px-4 py-2 text-sm font-medium transition hover:bg-white/15"> | ||
| View teaching formats | ||
| <span className="text-lg">→</span> |
There was a problem hiding this comment.
The buttons on lines 70 and 91 have no onClick handlers or proper interactive functionality. They appear to be placeholders for future navigation. Consider adding disabled={true} or aria-disabled="true" attributes, or implement the actual navigation behavior, or add TODO comments explaining what they should link to.
| > | ||
| <Navbar /> | ||
| {children} | ||
| <div className="relative min-h-screen bg-gradient-to-br from-[#d7e3ff] via-[#f3f5ff] to-[#e8ecff] dark:bg-slate-950"> |
There was a problem hiding this comment.
The layout applies the same gradient background (from-[#d7e3ff] via-[#f3f5ff] to-[#e8ecff]) to all pages globally. However, individual pages like app/page.tsx and app/about/page.tsx also define the same gradient in their root div elements, causing duplication. Consider removing the gradient from either the layout or individual pages to avoid redundant styling and improve maintainability.
| <div className="relative min-h-screen bg-gradient-to-br from-[#d7e3ff] via-[#f3f5ff] to-[#e8ecff] dark:bg-slate-950"> | |
| <div className="relative min-h-screen dark:bg-slate-950"> |
|
This PR is obsolete, I created a new PR implementing this functionality |
Description
Add initial seeding data and logic for learning resources to support development/demo workflows
Type of Change
expected)
Related Issues
Changes Made
Testing
Test Cases
Manual Testing
Screenshots/Videos
Before
N/A
After
N/A