Skip to content

feat: datenraum prototype (keep) #4327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e3fdc54
feat: datenraum prototype init
elbotho Dec 1, 2024
a3c9dd9
yarn
elbotho Dec 1, 2024
8db113b
add more icons
elbotho Dec 1, 2024
8e2722f
prettier etc
elbotho Dec 1, 2024
f144cd4
solve zindex issue
elbotho Dec 3, 2024
33fbace
Merge branch 'staging' into datenraum-prototype
elbotho Jan 16, 2025
8fd9951
fix(datenraum): use new endpoint and credentials
elbotho Jan 16, 2025
89d9f51
feat(datenraum): add node fetch (wip – no content yet)
elbotho Jan 16, 2025
a41a0d8
feat(datenraum): PUT to datenraum on save
elbotho Jan 16, 2025
4fae7d5
feat(datenraum): add content to node fetch
elbotho Jan 16, 2025
183d0d0
chore(datenraum): remove console log
elbotho Jan 16, 2025
733745c
fix(datenraum): use post to access serlos api
elbotho Jan 17, 2025
e167ba9
feat: add "own content"
elbotho Jan 27, 2025
add012c
more ui/flow improvements
elbotho Jan 27, 2025
64e7bef
change put variables
elbotho Jan 29, 2025
c476648
remove unused import
elbotho Jan 29, 2025
ad5587f
simplify save modal share ui
elbotho Jan 30, 2025
9ff716b
adapt types to provided data
elbotho Feb 5, 2025
f2d4f88
chore(yarn): Add "eslint-config-turbo"
kulla Feb 6, 2025
10f62a8
fix: Only serialize the serlo content once
kulla Feb 6, 2025
d17612c
fix(datenraum): Show toast only once
kulla Feb 6, 2025
604c493
feat: Add hint
kulla Feb 6, 2025
8b4f7f0
feat: Load content from Datenraum
kulla Feb 7, 2025
7d163b3
feat: Return to main page after save
kulla Feb 7, 2025
f470357
fix(datenraum): Generate new random ID for new content
kulla Feb 7, 2025
3dddb08
feat: Better default description
kulla Feb 7, 2025
3d3b791
fix: Remove width and height from datenraum SVG
kulla Feb 8, 2025
d88a43a
feat: Add simple password protection
kulla Feb 19, 2025
6c8c896
Merge remote-tracking branch 'origin/staging' into datenraum-prototype
kulla Feb 20, 2025
dea3606
feat: Check password on enter
kulla Feb 20, 2025
3c28723
fix: Save password in cookies
kulla Feb 20, 2025
f2f3e38
fix: Fix TS error
kulla Feb 20, 2025
5b5237b
fix: Run "yarn format"
kulla Feb 20, 2025
897d988
feat: Load content from Serlo if possible
kulla Mar 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12 changes: 11 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,24 @@
"@lumieducation/h5p-react": "^9.3.2",
"@ory/client": "^1.15.10",
"@ory/integrations": "1.1.5",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@serlo/authorization": "^0.60.0",
"@serlo/editor": "workspace:*",
"@serlo/editor": "workspace:^",
"@serlo/katex-styles": "1.0.1",
"@tanstack/react-query": "^5.62.0",
"@tippyjs/react": "^4.2.6",
"@uidotdev/usehooks": "^2.4.1",
"@vidstack/react": "next",
"array-move": "^4.0.0",
"autoprefixer": "^10.4.20",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fast-xml-parser": "^4.5.0",
"fp-ts": "^2.16.9",
Expand All @@ -62,6 +70,7 @@
"js-cookie": "^3.0.5",
"json-diff": "^1.0.6",
"katex": "^0.16.21",
"lucide-react": "^0.462.0",
"mathjs": "^13.2.2",
"mathlive": "^0.101.1",
"next": "^14.2.18",
Expand All @@ -77,6 +86,7 @@
"react-lazyload": "^3.2.1",
"react-textarea-autosize": "^8.5.5",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.5",
"timeago-react": "^3.0.6",
"timeago.js": "^4.0.2",
"tippy.js": "^6.3.7",
Expand Down
103 changes: 103 additions & 0 deletions apps/web/src/components/datenraum/login-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useMutation } from '@tanstack/react-query'
import clsx from 'clsx'
import Cookies from 'js-cookie'
import React, { createContext, useEffect } from 'react'

export const PasswordContext = createContext<string>('')

interface LoginFormProps {
children: React.ReactNode
}

export default function LoginForm(props: LoginFormProps) {
const [password, setPassword] = React.useState('')
const [isCorrectPassword, setIsCorrectPassword] = React.useState(false)

const cookieName = 'passwordForPrototype'

const checkPassword = useMutation({
mutationFn: async () => {
const response = await fetch(
`/api/datenraum/check-password?password=${encodeURIComponent(password)}`,
{ method: 'POST' }
)
if (!response.ok) {
throw new Error('Wrong password')
}
return response.json() as unknown
},
onSuccess: () => setIsCorrectPassword(true),
})

useEffect(() => {
const passwordCookieValue = Cookies.get(cookieName)

if (passwordCookieValue && !isCorrectPassword) {
setPassword(passwordCookieValue)
setIsCorrectPassword(true)
}
}, [])

useEffect(() => {
const passwordCookieValue = Cookies.get(cookieName)

console.log(passwordCookieValue, password)

if (isCorrectPassword && passwordCookieValue !== password) {
console.log('setting cookie')
Cookies.set(cookieName, password, { expires: 30 })
}
}, [password, isCorrectPassword])

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}

return isCorrectPassword ? renderChildren() : renderLoginForm()

function renderChildren() {
return (
<PasswordContext.Provider value={password}>
{props.children}
</PasswordContext.Provider>
)
}

function renderLoginForm() {
return (
<div>
<div className="mx-auto max-w-md p-8">
<h1 className="mb-4 text-3xl font-bold">Login</h1>
<div className="space-y-4">
<div>
<label htmlFor="password" className="mb-1 block">
Password:
</label>
<input
id="password"
className="w-full rounded border border-gray-300 px-3 py-2"
type="password"
value={password}
onChange={handleChange}
onKeyDown={(e) => e.key === 'Enter' && checkPassword.mutate()}
/>
</div>
<button
className={clsx(
'w-full rounded py-2 font-bold text-white',
password.length ? 'bg-gray-700' : 'bg-gray-200'
)}
onClick={() => checkPassword.mutate()}
disabled={!password.length}
>
Sign in
</button>
{checkPassword.isError && (
<div className="text-red-500">Wrong password</div>
)}
</div>
</div>
</div>
)
}
}
176 changes: 176 additions & 0 deletions apps/web/src/components/datenraum/search-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use client'

import { AnyEditorDocument } from '@editor/types/editor-plugins'
import { isArticleDocument } from '@editor/types/plugin-type-guards'
import { useQuery } from '@tanstack/react-query'
import { ImportIcon, NewspaperIcon, SquareCheckBigIcon } from 'lucide-react'
import { useRouter } from 'next/router'
import { useContext, useState } from 'react'

import { PasswordContext } from './login-form'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { EditorRenderer } from '@/serlo-editor-integration/editor-renderer'

const iconMap = {
Article: NewspaperIcon,
Exercise: SquareCheckBigIcon,
}

export const typeTitleMap = {
Article: 'Artikel',
Exercise: 'Aufgabe',
Course: 'Kurs',
} as const

export interface LearningResource {
id: string
url: string
title: string
description: string
type: 'Article' | 'Exercise'
}

async function fetchContent({
id,
password,
}: {
id: string
password: string
}) {
const fetchUrl = `/api/datenraum/node?id=${id}&password=${encodeURIComponent(password)}`
try {
const result = await fetch(fetchUrl)
const stateObject = (await result.json()) as {
editorState: AnyEditorDocument
}

const editorState = stateObject.editorState

if (isArticleDocument(editorState)) {
return editorState
}
// TODO: check for exercise later
return editorState
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error fetching content:', error)
throw new Error('Error fetching content')
}
}

export default function SearchCard({
entry,
onImport,
}: {
entry: LearningResource
onImport?: (state?: unknown) => void
}) {
const password = useContext(PasswordContext)
const router = useRouter()

const IconComponent = iconMap[entry.type]

const [enabled, setEnabled] = useState(false)

const { data } = useQuery({
queryKey: ['contentState', entry.id],
queryFn: ({ queryKey }) => fetchContent({ id: queryKey[1], password }),
enabled,
})

return (
<Dialog>
<DialogTrigger asChild>
<Card
key={entry.url}
className="flex cursor-pointer flex-col justify-between"
onClick={() => setEnabled(true)}
onMouseEnter={() => setEnabled(true)}
>
<CardHeader className="flex flex-row items-center gap-2 pb-2">
<CardTitle className="text-lg">{entry.title}</CardTitle>
</CardHeader>
<CardContent className="pb-6">
<div className="flex flex-wrap gap-2">{renderBadges()}</div>
</CardContent>
</Card>
</DialogTrigger>
<DialogContent className="z-[200] sm:max-w-2xl">
<DialogHeader>
<DialogTitle>{entry.title}</DialogTitle>
<DialogDescription className="py-3 text-gray-800">
{/* {entry.description} */}
</DialogDescription>
<div className="flex flex-wrap justify-between">
<a
href={entry.url}
target="_blank"
className="text-sm font-bold text-gray-400"
rel="noreferrer"
>
{entry.url}
</a>
<div className="flex gap-2">{renderBadges()}</div>
</div>
</DialogHeader>
<div>
<span className="text-sm font-bold text-sky-300">Vorschau:</span>
<div className="max-h-[60vh] overflow-y-auto border-y-2 border-sky-200 ">
<div className="w-[125%] origin-top-left scale-75 pt-5">
<EditorRenderer document={data} />
</div>
</div>
</div>
<Button
className="mt-3 w-full bg-sky-300 font-bold text-stone-800 hover:bg-orange-200"
onClick={() => {
if (onImport) onImport(data)
// TODO: this approach will not work any more now =/
else void router.push(`/entity/repository/add-revision/${entry.id}`)
}}
>
<ImportIcon /> Importieren
</Button>
</DialogContent>
</Dialog>
)

function renderBadges() {
return (
<>
<Badge
variant="secondary"
className="bg-sky-200 text-sky-700 hover:bg-sky-200"
>
{getSource(entry)}
</Badge>
<Badge variant="secondary">
{IconComponent ? (
<IconComponent className="mr-1 h-4 w-4 text-gray-400" />
) : null}{' '}
{typeTitleMap[entry.type]}
</Badge>
</>
)
}
}

function getSource(resource: LearningResource) {
if (resource.url.includes('serlo')) {
return 'Serlo'
} else if (resource.url.includes('vhs')) {
return 'VHS'
} else {
return 'Datenraum'
}
}
Loading
Loading