Loading portal-gui/src/components/common/expandable-markdown.tsx 0 → 100644 +57 −0 Changes for portal-gui/src/components/common/expandable-markdown.tsx: 57 added lines, 0 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { ChevronDown } from 'lucide-react'; import { Markdown } from './markdown'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; export function ExpandableMarkdown({ children }: { children: string }) { const [expanded, setExpanded] = useState(false); const paragraphs = children.trim().split(/\n\s*\n/); let previewLength = 0; let wordCount = 0; // Keep complete paragraphs and avoid leaving a heading without its content. while ( previewLength < paragraphs.length && (wordCount < 120 || /^(?:#{1,6} .+|\*\*[^\n]+\*\*)$/.test( paragraphs[previewLength - 1] ?? '', )) ) { wordCount += paragraphs[previewLength]?.split(/\s+/).length ?? 0; previewLength += 1; } const preview = paragraphs.slice(0, previewLength).join('\n\n'); const remaining = paragraphs.slice(previewLength).join('\n\n'); return ( <Collapsible open={expanded} onOpenChange={setExpanded} className="space-y-4" > <Markdown>{preview}</Markdown> {remaining && ( <> <CollapsibleContent> <Markdown>{remaining}</Markdown> </CollapsibleContent> <CollapsibleTrigger asChild> <Button variant="outline" size="sm" className="group"> {expanded ? 'Show less' : 'Read full description'} <ChevronDown aria-hidden className="transition-transform group-data-[state=open]:rotate-180" /> </Button> </CollapsibleTrigger> </> )} </Collapsible> ); } portal-gui/src/components/common/site-footer.tsx +3 −4 Changes for portal-gui/src/components/common/site-footer.tsx: 3 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -2,16 +2,15 @@ export function SiteFooter() { return ( <footer role="contentinfo" className="mt-auto flex shrink-0 flex-wrap items-center justify-between gap-2 border-t px-4 py-3 text-sm text-muted-foreground lg:px-6" className="mt-auto flex shrink-0 items-center border-t px-4 py-3 text-sm text-muted-foreground lg:px-6" > <span>OOP Portal · Open Operator Platform</span> <a href="https://labs.etsi.org/rep/oop/code/portal" href="https://labs.etsi.org/rep/oop" target="_blank" rel="noopener noreferrer" className="font-medium underline-offset-4 hover:text-foreground hover:underline" > Source code Open Operator Platform by ETSI </a> </footer> ); Loading portal-gui/src/features/apis/api-card.tsx +6 −0 Changes for portal-gui/src/features/apis/api-card.tsx: 6 added lines, 0 removed lines. Original line number Diff line number Diff line import { ROUTES } from '@/app/config/route'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import type { ApiCardProps } from '@/types/api'; import { Link } from 'react-router-dom'; Loading @@ -9,7 +10,12 @@ export function ApiCard({ api }: ApiCardProps) { <Card className="group h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"> <CardHeader> <CardTitle className="text-2xl">{api.title}</CardTitle> <div className="flex flex-wrap items-center gap-2"> <p className="font-semibold text-primary">{api.category}</p> {api.version && ( <Badge variant="outline">Version: {api.version}</Badge> )} </div> </CardHeader> <CardContent className="space-y-4"> <p className="text-muted-foreground">{api.description}</p> Loading portal-gui/src/features/apis/api-details-page.tsx +87 −91 Changes for portal-gui/src/features/apis/api-details-page.tsx: 87 added lines, 91 removed lines. Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ import { useOutletContext } from 'react-router-dom'; import { SectionTitle } from '@/components/common/section-title'; import { PageContent } from '@/components/common/page-content'; import { Markdown } from '@/components/common/markdown'; import { Separator } from '@/components/ui/separator'; import { ExpandableMarkdown } from '@/components/common/expandable-markdown'; import { Card, CardContent, Loading @@ -29,41 +29,104 @@ export function ApiDetailsPage() { <div className="mx-auto w-full space-y-8 px-4 py-6 lg:px-6"> <Card className="overflow-hidden"> <CardHeader className="border-b bg-muted/20"> <div className="space-y-4"> <div> <CardTitle className="text-2xl tracking-tight sm:text-3xl"> {api.title} <CardTitle className="text-xl font-semibold text-sidebar-primary"> Overview </CardTitle> </CardHeader> <CardContent className="space-y-4 px-6 py-6"> <ExpandableMarkdown key={api.id}> {documentation?.description ?? api.description} </ExpandableMarkdown> </CardContent> </Card> {useCases?.length || benefits?.length ? ( <section className="space-y-4"> <SectionTitle title="Use cases and benefits" /> <div className="grid gap-6 md:grid-cols-2"> {useCases && useCases.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Common use cases </CardTitle> <p className="mt-2 text-sm font-medium text-primary"> {api.category} <CardDescription> Examples of how this API can be used. </CardDescription> </CardHeader> <CardContent> <div className="grid gap-3"> {useCases.map((useCase) => ( <div key={useCase.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {useCase.title} </h3> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {useCase.description} </p> </div> ))} </div> </CardContent> </Card> )} {benefits && benefits.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Benefits </CardTitle> <CardDescription> How this API can support your applications. </CardDescription> </CardHeader> <CardContent className="px-6 py-6"> <h3 className="text-xl font-semibold text-sidebar-primary"> Description <CardContent> <div className="grid gap-3"> {benefits.map((benefit) => ( <div key={benefit.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {benefit.title} </h3> <div className="mt-2"> <Markdown> {documentation?.description ?? api.description} </Markdown> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {benefit.description} </p> </div> ))} </div> </CardContent> </Card> <section className="space-y-4"> <div className="flex items-center gap-4"> <Separator className="flex-1 bg-primary" /> <h2 className="shrink-0 text-sm font-semibold tracking-wider text-muted-foreground text-primary uppercase"> API Usage </h2> <Separator className="flex-1 bg-primary" /> )} </div> </section> ) : null} <section className="space-y-4"> <SectionTitle title="API Usage" /> <div className="space-y-6"> {documentation?.instructions && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Usage guidance </CardTitle> </CardHeader> <CardContent> <Markdown>{documentation.instructions}</Markdown> </CardContent> </Card> )} <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Loading @@ -75,9 +138,6 @@ export function ApiDetailsPage() { </CardHeader> <CardContent className="space-y-5"> {documentation?.instructions && ( <Markdown>{documentation.instructions}</Markdown> )} {api.operations && api.operations.length > 0 ? ( <div> <ul className="grid gap-3 md:grid-cols-2"> Loading Loading @@ -125,80 +185,16 @@ export function ApiDetailsPage() { )} </CardContent> </Card> {useCases && useCases.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Common use cases </CardTitle> <CardDescription> Examples of how this API can be used. </CardDescription> </CardHeader> <CardContent> <div className="grid gap-3 md:grid-cols-2"> {useCases.map((useCase) => ( <div key={useCase.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {useCase.title} </h3> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {useCase.description} </p> </div> ))} </div> </CardContent> </Card> )} {benefits && benefits.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Benefits </CardTitle> <CardDescription> How this API can support your applications. </CardDescription> </CardHeader> <CardContent> <div className="grid gap-3 md:grid-cols-2"> {benefits.map((benefit) => ( <div key={benefit.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {benefit.title} </h3> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {benefit.description} </p> </div> ))} </div> </CardContent> </Card> )} </div> </section> <section className="space-y-4"> <SectionTitle title="API Documentation" /> <SectionTitle title="API reference" /> <Card className="overflow-hidden"> <CardHeader className="border-b bg-muted/20"> <CardTitle className="text-xl font-semibold text-sidebar-primary"> OpenAPI Documentation Interactive API reference </CardTitle> <p className="text-sm text-muted-foreground"> Loading portal-gui/src/features/apis/api-workspace.tsx +6 −0 Changes for portal-gui/src/features/apis/api-workspace.tsx: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import { ArrowLeft, BookOpen, Compass } from 'lucide-react'; import { Link, Outlet, useLocation, useParams } from 'react-router-dom'; import { ROUTES } from '@/app/config/route'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { PageState } from '@/components/common/page-state'; import { useApi } from '@/hooks/useAPIs'; Loading Loading @@ -34,7 +35,12 @@ export function ApiWorkspacePage() { <h1 className="text-2xl font-semibold tracking-tight"> {api.title} </h1> <div className="flex flex-wrap items-center gap-2"> <p className="text-muted-foreground">{api.category}</p> {api.version && ( <Badge variant="outline">Version: {api.version}</Badge> )} </div> </div> <nav aria-label="API sections" className="flex gap-2"> <Button Loading Loading
portal-gui/src/components/common/expandable-markdown.tsx 0 → 100644 +57 −0 Changes for portal-gui/src/components/common/expandable-markdown.tsx: 57 added lines, 0 removed lines. Original line number Diff line number Diff line import { useState } from 'react'; import { ChevronDown } from 'lucide-react'; import { Markdown } from './markdown'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; export function ExpandableMarkdown({ children }: { children: string }) { const [expanded, setExpanded] = useState(false); const paragraphs = children.trim().split(/\n\s*\n/); let previewLength = 0; let wordCount = 0; // Keep complete paragraphs and avoid leaving a heading without its content. while ( previewLength < paragraphs.length && (wordCount < 120 || /^(?:#{1,6} .+|\*\*[^\n]+\*\*)$/.test( paragraphs[previewLength - 1] ?? '', )) ) { wordCount += paragraphs[previewLength]?.split(/\s+/).length ?? 0; previewLength += 1; } const preview = paragraphs.slice(0, previewLength).join('\n\n'); const remaining = paragraphs.slice(previewLength).join('\n\n'); return ( <Collapsible open={expanded} onOpenChange={setExpanded} className="space-y-4" > <Markdown>{preview}</Markdown> {remaining && ( <> <CollapsibleContent> <Markdown>{remaining}</Markdown> </CollapsibleContent> <CollapsibleTrigger asChild> <Button variant="outline" size="sm" className="group"> {expanded ? 'Show less' : 'Read full description'} <ChevronDown aria-hidden className="transition-transform group-data-[state=open]:rotate-180" /> </Button> </CollapsibleTrigger> </> )} </Collapsible> ); }
portal-gui/src/components/common/site-footer.tsx +3 −4 Changes for portal-gui/src/components/common/site-footer.tsx: 3 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -2,16 +2,15 @@ export function SiteFooter() { return ( <footer role="contentinfo" className="mt-auto flex shrink-0 flex-wrap items-center justify-between gap-2 border-t px-4 py-3 text-sm text-muted-foreground lg:px-6" className="mt-auto flex shrink-0 items-center border-t px-4 py-3 text-sm text-muted-foreground lg:px-6" > <span>OOP Portal · Open Operator Platform</span> <a href="https://labs.etsi.org/rep/oop/code/portal" href="https://labs.etsi.org/rep/oop" target="_blank" rel="noopener noreferrer" className="font-medium underline-offset-4 hover:text-foreground hover:underline" > Source code Open Operator Platform by ETSI </a> </footer> ); Loading
portal-gui/src/features/apis/api-card.tsx +6 −0 Changes for portal-gui/src/features/apis/api-card.tsx: 6 added lines, 0 removed lines. Original line number Diff line number Diff line import { ROUTES } from '@/app/config/route'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import type { ApiCardProps } from '@/types/api'; import { Link } from 'react-router-dom'; Loading @@ -9,7 +10,12 @@ export function ApiCard({ api }: ApiCardProps) { <Card className="group h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg"> <CardHeader> <CardTitle className="text-2xl">{api.title}</CardTitle> <div className="flex flex-wrap items-center gap-2"> <p className="font-semibold text-primary">{api.category}</p> {api.version && ( <Badge variant="outline">Version: {api.version}</Badge> )} </div> </CardHeader> <CardContent className="space-y-4"> <p className="text-muted-foreground">{api.description}</p> Loading
portal-gui/src/features/apis/api-details-page.tsx +87 −91 Changes for portal-gui/src/features/apis/api-details-page.tsx: 87 added lines, 91 removed lines. Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ import { useOutletContext } from 'react-router-dom'; import { SectionTitle } from '@/components/common/section-title'; import { PageContent } from '@/components/common/page-content'; import { Markdown } from '@/components/common/markdown'; import { Separator } from '@/components/ui/separator'; import { ExpandableMarkdown } from '@/components/common/expandable-markdown'; import { Card, CardContent, Loading @@ -29,41 +29,104 @@ export function ApiDetailsPage() { <div className="mx-auto w-full space-y-8 px-4 py-6 lg:px-6"> <Card className="overflow-hidden"> <CardHeader className="border-b bg-muted/20"> <div className="space-y-4"> <div> <CardTitle className="text-2xl tracking-tight sm:text-3xl"> {api.title} <CardTitle className="text-xl font-semibold text-sidebar-primary"> Overview </CardTitle> </CardHeader> <CardContent className="space-y-4 px-6 py-6"> <ExpandableMarkdown key={api.id}> {documentation?.description ?? api.description} </ExpandableMarkdown> </CardContent> </Card> {useCases?.length || benefits?.length ? ( <section className="space-y-4"> <SectionTitle title="Use cases and benefits" /> <div className="grid gap-6 md:grid-cols-2"> {useCases && useCases.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Common use cases </CardTitle> <p className="mt-2 text-sm font-medium text-primary"> {api.category} <CardDescription> Examples of how this API can be used. </CardDescription> </CardHeader> <CardContent> <div className="grid gap-3"> {useCases.map((useCase) => ( <div key={useCase.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {useCase.title} </h3> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {useCase.description} </p> </div> ))} </div> </CardContent> </Card> )} {benefits && benefits.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Benefits </CardTitle> <CardDescription> How this API can support your applications. </CardDescription> </CardHeader> <CardContent className="px-6 py-6"> <h3 className="text-xl font-semibold text-sidebar-primary"> Description <CardContent> <div className="grid gap-3"> {benefits.map((benefit) => ( <div key={benefit.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {benefit.title} </h3> <div className="mt-2"> <Markdown> {documentation?.description ?? api.description} </Markdown> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {benefit.description} </p> </div> ))} </div> </CardContent> </Card> <section className="space-y-4"> <div className="flex items-center gap-4"> <Separator className="flex-1 bg-primary" /> <h2 className="shrink-0 text-sm font-semibold tracking-wider text-muted-foreground text-primary uppercase"> API Usage </h2> <Separator className="flex-1 bg-primary" /> )} </div> </section> ) : null} <section className="space-y-4"> <SectionTitle title="API Usage" /> <div className="space-y-6"> {documentation?.instructions && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Usage guidance </CardTitle> </CardHeader> <CardContent> <Markdown>{documentation.instructions}</Markdown> </CardContent> </Card> )} <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Loading @@ -75,9 +138,6 @@ export function ApiDetailsPage() { </CardHeader> <CardContent className="space-y-5"> {documentation?.instructions && ( <Markdown>{documentation.instructions}</Markdown> )} {api.operations && api.operations.length > 0 ? ( <div> <ul className="grid gap-3 md:grid-cols-2"> Loading Loading @@ -125,80 +185,16 @@ export function ApiDetailsPage() { )} </CardContent> </Card> {useCases && useCases.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Common use cases </CardTitle> <CardDescription> Examples of how this API can be used. </CardDescription> </CardHeader> <CardContent> <div className="grid gap-3 md:grid-cols-2"> {useCases.map((useCase) => ( <div key={useCase.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {useCase.title} </h3> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {useCase.description} </p> </div> ))} </div> </CardContent> </Card> )} {benefits && benefits.length > 0 && ( <Card> <CardHeader> <CardTitle className="text-xl font-semibold text-sidebar-primary"> Benefits </CardTitle> <CardDescription> How this API can support your applications. </CardDescription> </CardHeader> <CardContent> <div className="grid gap-3 md:grid-cols-2"> {benefits.map((benefit) => ( <div key={benefit.title} className="rounded-md border bg-muted/30 p-4" > <h3 className="text-sm font-semibold"> {benefit.title} </h3> <p className="mt-1 text-sm leading-6 text-muted-foreground"> {benefit.description} </p> </div> ))} </div> </CardContent> </Card> )} </div> </section> <section className="space-y-4"> <SectionTitle title="API Documentation" /> <SectionTitle title="API reference" /> <Card className="overflow-hidden"> <CardHeader className="border-b bg-muted/20"> <CardTitle className="text-xl font-semibold text-sidebar-primary"> OpenAPI Documentation Interactive API reference </CardTitle> <p className="text-sm text-muted-foreground"> Loading
portal-gui/src/features/apis/api-workspace.tsx +6 −0 Changes for portal-gui/src/features/apis/api-workspace.tsx: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import { ArrowLeft, BookOpen, Compass } from 'lucide-react'; import { Link, Outlet, useLocation, useParams } from 'react-router-dom'; import { ROUTES } from '@/app/config/route'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { PageState } from '@/components/common/page-state'; import { useApi } from '@/hooks/useAPIs'; Loading Loading @@ -34,7 +35,12 @@ export function ApiWorkspacePage() { <h1 className="text-2xl font-semibold tracking-tight"> {api.title} </h1> <div className="flex flex-wrap items-center gap-2"> <p className="text-muted-foreground">{api.category}</p> {api.version && ( <Badge variant="outline">Version: {api.version}</Badge> )} </div> </div> <nav aria-label="API sections" className="flex gap-2"> <Button Loading