Commit af2dce9b authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: enhance API UI with version display, add expandable markdown component for descriptions

parent 6113d747
Loading
Loading
Loading
Loading
+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>
  );
}
+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
@@ -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>
  );
+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';

@@ -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>
+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
@@ -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,
@@ -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">
@@ -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">
@@ -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">
+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
@@ -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';

@@ -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