Skip to content
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

Home page (minus Storybook) #91

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 57 additions & 0 deletions webapp/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Promise<ProjectCardProps>>(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 <HomeDashboard projects={projects} />;
}
46 changes: 46 additions & 0 deletions webapp/src/components/HomeDashboard.tsx
Original file line number Diff line number Diff line change
@@ -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<HomeDashboardProps> = ({ projects }) => {
return (
<Box
alignItems="center"
display="flex"
flexDirection="column"
justifyContent="center"
minHeight="97vh"
>
<Typography alignSelf="flex-start" color="primary" component="h1">
Your Lyra Projects
</Typography>
<List
sx={{
'@media (min-width: 600px)': {
alignContent: 'center',
columnGap: 2,
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 300px));',
justifyContent: 'center',
},
alignItems: 'center',
display: 'flex',
flex: 1,
flexDirection: 'column',
rowGap: 2,
width: '100%',
}}
>
{projects.map((project, i) => (
<ProjectCard key={i} {...project} />
))}
</List>
</Box>
);
};

export default HomeDashboard;
172 changes: 172 additions & 0 deletions webapp/src/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -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<ProjectCardProps> = ({
href,
languages,
name,
messageCount,
}) => {
return (
<Box component="li" sx={{ listStyleType: 'none' }} width="100%">
<Box
bgcolor="neutral.50"
border={1}
borderColor="transparent"
borderRadius={8}
display="flex"
flexDirection="column"
position="relative"
px={3}
py={2}
rowGap={1}
sx={{
':focus-within, :hover': {
outlineColor: 'focusVisible',
outlineStyle: 'solid',
outlineWidth: 1,
},
}}
>
<Typography component="h2">
<Link
href={href}
sx={{
'::after': {
bottom: 0,
content: '""',
left: 0,
position: 'absolute',
right: 0,
top: 0,
width: '100%',
},
':hover, :focus': {
outline: 'none',
textDecoration: 'none',
},
color: 'inherit',
position: 'inherit',
}}
>
{name}
</Link>
</Typography>
<Typography>{messageCount} messages</Typography>
<Box
columnGap={1}
component="ul"
display="grid"
margin={0}
padding={0}
rowGap={1}
sx={{
gridTemplateColumns: 'repeat(auto-fit, minmax(30px, 120px));',
width: '100%',
}}
>
{languages.map(({ href, language, progress }) => (
<Box
key={language}
bgcolor="primary.50"
borderRadius={4}
component="li"
position="relative"
sx={{
':focus-within, :hover': {
outlineColor: 'focusVisible',
outlineStyle: 'solid',
outlineWidth: 1,
},
listStyleType: 'none',
}}
>
<Box display="flex" flexDirection="column" px={3} py={2}>
<Link
href={href}
sx={{
'::after': {
bottom: 0,
content: '""',
left: 0,
position: 'absolute',
right: 0,
top: 0,
width: '100%',
},
':hover, :focus': {
outline: 'none',
textDecoration: 'none',
},
position: 'inherit',
}}
>
{language}
</Link>
<LinearProgress
determinate
size="lg"
sx={{ backgroundColor: '#ffffff' }}
thickness={8}
value={Math.min(progress, 100)}
variant="outlined"
/>
</Box>
</Box>
))}
</Box>
</Box>
</Box>
);
};

export default ProjectCard;