My App
LoopKit

Getting Started

Install and set up LoopKit in your NestJS backend and React frontend

Prerequisites

  • Bun or Node.js 18+
  • A NestJS application (backend)
  • A React 18/19 application (frontend)

Installation

Backend

bun add @loopkit/nestjs

Peer dependencies (install as needed):

# Required for Prisma adapter
bun add @prisma/client

# Optional content transforms
bun add marked        # Markdown rendering
bun add katex         # Math rendering
bun add highlight.js  # Code syntax highlighting
bun add sanitize-html # XSS protection

Frontend

bun add @loopkit/react

Peer dependencies:

bun add react react-dom  # React 18 or 19

Backend Setup

1. Register the Module

import { Module } from '@nestjs/common';
import { LoopKitModule, PrismaLoopKitAdapter } from '@loopkit/nestjs';
import { PrismaService } from './prisma.service';

@Module({
    imports: [
        LoopKitModule.register({
            storage: new PrismaLoopKitAdapter(new PrismaService()),
        }),
    ],
})
export class AppModule {}

2. Create a Controller

import { Controller, Post, Body, Param } from '@nestjs/common';
import { ReviewSessionService, DeckService } from '@loopkit/nestjs';

@Controller('loopkit')
export class FlashcardController {
    constructor(
        private readonly session: ReviewSessionService,
        private readonly decks: DeckService,
    ) {}

    @Post('decks/:deckId/study')
    async buildQueue(@Param('deckId') deckId: string) {
        return this.session.buildQueue(deckId);
    }

    @Post('cards/:cardId/grade')
    async gradeCard(
        @Param('cardId') cardId: string,
        @Body() body: { grade: string; timeTakenMs: number },
    ) {
        return this.session.gradeCard(cardId, body.grade as any, body.timeTakenMs);
    }
}

3. Seed Default Note Types

On application startup, seed the default note types:

import { NoteTypeService } from '@loopkit/nestjs';

@Injectable()
export class AppInitService implements OnModuleInit {
    constructor(private readonly noteTypes: NoteTypeService) {}

    async onModuleInit() {
        await this.noteTypes.seedDefaults();
    }
}

Frontend Setup

1. Wrap Your App with the Provider

import { LoopKitProvider } from '@loopkit/react';
import '@loopkit/react/styles.css';

function App() {
    return (
        <LoopKitProvider apiUrl="/api/loopkit">
            <YourApp />
        </LoopKitProvider>
    );
}

2. Use the Review Session Component

import { ReviewSession } from '@loopkit/react';

function StudyPage({ deckId }: { deckId: string }) {
    return (
        <ReviewSession
            deckId={deckId}
            onComplete={(summary) => {
                console.log(`Reviewed ${summary.totalReviewed} cards`);
            }}
        />
    );
}

3. Or Build a Custom UI with Hooks

import { useReviewSession } from '@loopkit/react';

function CustomStudy({ deckId }: { deckId: string }) {
    const {
        sessionState,
        currentCard,
        renderedContent,
        nextIntervals,
        progress,
        startSession,
        showAnswer,
        grade,
    } = useReviewSession();

    if (sessionState === 'idle') {
        return <button onClick={() => startSession(deckId)}>Start Study</button>;
    }

    if (sessionState === 'reviewing' && renderedContent) {
        return (
            <div>
                <p>{progress.reviewed} / {progress.total}</p>
                <div dangerouslySetInnerHTML={{ __html: renderedContent.front }} />
                <button onClick={showAnswer}>Show Answer</button>
            </div>
        );
    }

    if (sessionState === 'answered' && renderedContent) {
        return (
            <div>
                <div dangerouslySetInnerHTML={{ __html: renderedContent.back }} />
                <div>
                    <button onClick={() => grade('again')}>Again {nextIntervals?.again}</button>
                    <button onClick={() => grade('hard')}>Hard {nextIntervals?.hard}</button>
                    <button onClick={() => grade('good')}>Good {nextIntervals?.good}</button>
                    <button onClick={() => grade('easy')}>Easy {nextIntervals?.easy}</button>
                </div>
            </div>
        );
    }

    return <p>Session complete!</p>;
}

Next Steps

On this page