diff --git a/webapp/src/app/page.tsx b/webapp/src/app/page.tsx new file mode 100644 index 00000000..af3f571b --- /dev/null +++ b/webapp/src/app/page.tsx @@ -0,0 +1,57 @@ +import { Cache } from '@/Cache'; +import HomeDashboard from '@/components/HomeDashboard'; +import MessageAdapterFactory from '@/utils/adapters/MessageAdapterFactory'; +import { ProjectCardProps } from '@/components/ProjectCard'; +import { RepoGit } from '@/RepoGit'; +import { ServerConfig } from '@/utils/serverConfig'; + +// Force dynamic rendering for this page. By default Next.js attempts to render +// this page statically. That means that it tries to render the page at build +// time instead of at runtime. That doesn't work: this page needs to fetch +// project-specific config files and perform git operations. So this little +// one-liner forces it into dynamic rendering mode. +// +// More info on dynamic vs static rendering at: +// https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering +// +// More info on `export const dynamic` at: +// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config +export const dynamic = 'force-dynamic'; + +export default async function Home() { + const serverConfig = await ServerConfig.read(); + const projects = await Promise.all( + serverConfig.projects.map>(async (project) => { + await RepoGit.cloneIfNotExist(project); + const repoGit = await RepoGit.getRepoGit(project); + const lyraConfig = await repoGit.getLyraConfig(); + const projectConfig = lyraConfig.getProjectConfigByPath( + project.projectPath, + ); + const msgAdapter = MessageAdapterFactory.createAdapter(projectConfig); + const messages = await msgAdapter.getMessages(); + const store = await Cache.getProjectStore(projectConfig); + const languages = await Promise.all( + projectConfig.languages.map(async (lang) => { + const translations = await store.getTranslations(lang); + return { + href: `/projects/${project.name}/${lang}`, + language: lang, + progress: translations + ? (Object.keys(translations).length / messages.length) * 100 + : 0, + }; + }), + ); + + return { + href: `/projects/${project.name}`, + languages, + messageCount: messages.length, + name: project.name, + }; + }), + ); + + return ; +} diff --git a/webapp/src/components/HomeDashboard.tsx b/webapp/src/components/HomeDashboard.tsx new file mode 100644 index 00000000..f78311b2 --- /dev/null +++ b/webapp/src/components/HomeDashboard.tsx @@ -0,0 +1,46 @@ +import { FC } from 'react'; +import { Box, List, Typography } from '@mui/joy'; +import ProjectCard, { ProjectCardProps } from './ProjectCard'; + +type HomeDashboardProps = { + projects: ProjectCardProps[]; +}; + +const HomeDashboard: FC = ({ projects }) => { + return ( + + + Your Lyra Projects + + + {projects.map((project, i) => ( + + ))} + + + ); +}; + +export default HomeDashboard; diff --git a/webapp/src/components/ProjectCard.tsx b/webapp/src/components/ProjectCard.tsx new file mode 100644 index 00000000..ff8e83ce --- /dev/null +++ b/webapp/src/components/ProjectCard.tsx @@ -0,0 +1,172 @@ +import { FC } from 'react'; +import { Box, LinearProgress, Link, Typography } from '@mui/joy'; + +export type ProjectCardProps = { + /** + * The URL of the project page. + */ + href: string; + + /** + * The project's languages and their translation progress. + */ + languages: { + /** + * The URL of the page containing the project's messages in this language. + */ + + href: string; + + /** + * The name of the language. + */ + language: string; + + /** + * The percentage of messages translated in this language. 0 means none, 100 + * means all of them. + */ + progress: number; + }[]; + + /** + * The number of messages in the project. + */ + messageCount: number; + + /** + * The name of the project. + */ + name: string; +}; + +/** + * Project cards are the primary navigation element on the home screen. They + * display information about the project, and clicking them takes the user to + * the project page. + * + * Displaying a collection of structured information like this in a clickable + * card introduces some accessibility challenges. The implementation here + * employs the [Inclusive Components "pseudo-content trick"](https://inclusive-components.design/cards/). + * + */ +const ProjectCard: FC = ({ + href, + languages, + name, + messageCount, +}) => { + return ( + + + + + {name} + + + {messageCount} messages + + {languages.map(({ href, language, progress }) => ( + + + + {language} + + + + + ))} + + + + ); +}; + +export default ProjectCard;