@ -0,0 +1,22 @@
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
export enum DeviceType {
|
||||
DESKTOP = 'DESKTOP',
|
||||
TABLET = 'TABLET',
|
||||
MOBILE = 'MOBILE',
|
||||
}
|
||||
|
||||
export const useDeviceType = () => {
|
||||
const isTablet = useMediaQuery({
|
||||
query: '(max-width: 1199px) and (min-width: 810px)',
|
||||
});
|
||||
const isMobile = useMediaQuery({ query: '(max-width: 809px)' });
|
||||
|
||||
if (isMobile) {
|
||||
return DeviceType.MOBILE;
|
||||
}
|
||||
if (isTablet) {
|
||||
return DeviceType.TABLET;
|
||||
}
|
||||
return DeviceType.DESKTOP;
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { ResponsiveTimeRange } from '@nivo/calendar';
|
||||
|
||||
import { CardContainer } from '@/app/_components/contributors/CardContainer';
|
||||
import { Title } from '@/app/_components/contributors/Title';
|
||||
|
||||
export const ActivityLog = ({
|
||||
data,
|
||||
}: {
|
||||
data: { value: number; day: string }[];
|
||||
}) => {
|
||||
if (!data.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<CardContainer>
|
||||
<Title>Activity</Title>
|
||||
<div style={{ width: '100%', height: '214px' }}>
|
||||
<ResponsiveTimeRange
|
||||
data={data}
|
||||
emptyColor="#F4EFFF"
|
||||
colors={['#E9DFFF', '#B28FFE', '#915FFD']}
|
||||
dayBorderWidth={2}
|
||||
dayBorderColor="#ffffff"
|
||||
dayRadius={4}
|
||||
daySpacing={2}
|
||||
/>
|
||||
</div>
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import Link from 'next/link';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
const AvatarGridContainer = styled.div`
|
||||
margin: 0 auto;
|
||||
max-width: 1024px;
|
||||
justify-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const AvatarItem = styled.div`
|
||||
position: relative;
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
border: 3px solid #141414;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
transition: 200ms;
|
||||
|
||||
&:hover {
|
||||
-webkit-box-shadow: -6px 6px 0px 1px rgba(0, 0, 0, 1);
|
||||
-moz-box-shadow: -6px 6px 0px 1px rgba(0, 0, 0, 1);
|
||||
box-shadow: -6px 6px 0px 1px rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.username {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
visibility 0.3s;
|
||||
}
|
||||
|
||||
&:hover .username {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const AvatarGrid = ({ users }: { users: User[] }) => {
|
||||
return (
|
||||
<AvatarGridContainer>
|
||||
{users.map((user) => (
|
||||
<Link href={`/contributors/${user.id}`} key={`l_${user.id}`}>
|
||||
<AvatarItem key={user.id}>
|
||||
<img src={user.avatarUrl} alt={user.id} />
|
||||
<span className="username">{user.id}</span>
|
||||
</AvatarItem>
|
||||
</Link>
|
||||
))}
|
||||
</AvatarGridContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarGrid;
|
||||
@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { Breadcrumbs } from '@/app/_components/ui/layout/Breadcrumbs';
|
||||
|
||||
const BREADCRUMB_ITEMS = [
|
||||
{
|
||||
uri: '/contributors',
|
||||
label: 'Contributors',
|
||||
},
|
||||
];
|
||||
|
||||
export const Breadcrumb = ({ active }: { active: string }) => {
|
||||
return (
|
||||
<Breadcrumbs items={BREADCRUMB_ITEMS} activePage={active} separator="/" />
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const CardContainer = styled.div`
|
||||
border: 3px solid #141414;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-direction: column;
|
||||
background-color: #fafafa;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
max-width: 898px;
|
||||
padding: 40px;
|
||||
|
||||
gap: 40px;
|
||||
@media (max-width: 809px) {
|
||||
width: 100%;
|
||||
padding: 40px 24px 40px 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContentContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return <Container>{children}</Container>;
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const Title = styled.h2`
|
||||
font-size: 56px;
|
||||
font-weight: 600;
|
||||
color: #b3b3b3;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 64px;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 28px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
Our amazing <br /> <span style={{ color: 'black' }}>Contributors</span>
|
||||
</Title>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { GithubIcon } from '@/app/_components/ui/icons/SvgIcons';
|
||||
|
||||
const ProfileContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Avatar = styled.div`
|
||||
border: 3px solid #141414;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
|
||||
const Details = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.username {
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 48px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 24px;
|
||||
line-height: 28.8px;
|
||||
}
|
||||
}
|
||||
|
||||
.duration {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #818181;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledGithubIcon = styled(GithubIcon)`
|
||||
@media (max-width: 810px) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ProfileCardProps {
|
||||
username: string;
|
||||
avatarUrl: string;
|
||||
firstContributionAt: string;
|
||||
}
|
||||
|
||||
export const ProfileCard = ({
|
||||
username,
|
||||
avatarUrl,
|
||||
firstContributionAt,
|
||||
}: ProfileCardProps) => {
|
||||
return (
|
||||
<ProfileContainer>
|
||||
<Avatar>
|
||||
<img src={avatarUrl} alt={username} />
|
||||
</Avatar>
|
||||
<Details>
|
||||
<h3 className="username">
|
||||
@{username}
|
||||
<a href={`https://github.com/${username}`} target="_blank">
|
||||
<StyledGithubIcon size="M" color="rgba(0,0,0,1)" />
|
||||
</a>
|
||||
</h3>
|
||||
{firstContributionAt && (
|
||||
<p className="duration">
|
||||
Contributing since{' '}
|
||||
{format(new Date(firstContributionAt), 'MMMM yyyy')}
|
||||
</p>
|
||||
)}
|
||||
</Details>
|
||||
</ProfileContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CardContainer } from '@/app/_components/contributors/CardContainer';
|
||||
|
||||
const Container = styled(CardContainer)`
|
||||
flex-direction: row;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
color: #b3b3b3;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
color: #474747;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 2px;
|
||||
background-color: #141414;
|
||||
border-radius: 40px;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface ProfileInfoProps {
|
||||
mergedPRsCount: number;
|
||||
rank: string;
|
||||
activeDays: number;
|
||||
}
|
||||
|
||||
export const ProfileInfo = ({
|
||||
mergedPRsCount,
|
||||
rank,
|
||||
activeDays,
|
||||
}: ProfileInfoProps) => {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<div className="item">
|
||||
<p className="title">Merged PR</p>
|
||||
<span className="value">{mergedPRsCount}</span>
|
||||
</div>
|
||||
<div className="separator"></div>
|
||||
<div className="item">
|
||||
<p className="title">Rank</p>
|
||||
<span className="value">{rank}%</span>
|
||||
</div>
|
||||
<div className="separator"></div>
|
||||
<div className="item">
|
||||
<p className="title">Active Days</p>
|
||||
<span className="value">{activeDays}</span>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,109 @@
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { PullRequestIcon } from '@/app/_components/ui/icons/SvgIcons';
|
||||
import { formatIntoRelativeDate } from '@/shared-utils/formatIntoRelativeDate';
|
||||
|
||||
const StyledTooltip = styled(Tooltip)``;
|
||||
|
||||
const Item = styled.div`
|
||||
display: flex;
|
||||
gap: 17px;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.a`
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #474747;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledPrLink = styled.a`
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: #474747;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 500;
|
||||
color: #b3b3b3;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledPullRequestIcon = styled(PullRequestIcon)`
|
||||
@media screen {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface PullRequestItemProps {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
mergedAt: string | null;
|
||||
authorId: string;
|
||||
}
|
||||
|
||||
export const PullRequestItem = ({
|
||||
id,
|
||||
title,
|
||||
url,
|
||||
createdAt,
|
||||
mergedAt,
|
||||
authorId,
|
||||
}: PullRequestItemProps) => {
|
||||
const prNumber = url.split('/').slice(-1)[0];
|
||||
return (
|
||||
<Item key={id}>
|
||||
<div>
|
||||
<StyledPullRequestIcon
|
||||
color={mergedAt ? '#915FFD' : '#1A7F37'}
|
||||
size="M"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<StyledTitle href={url} target="_blank">
|
||||
{title}
|
||||
</StyledTitle>
|
||||
<StyledDescription>
|
||||
<StyledPrLink
|
||||
href={'https://github.com/twentyhq/twenty/pull/' + prNumber}
|
||||
target="__blank"
|
||||
>
|
||||
#{prNumber}
|
||||
</StyledPrLink>{' '}
|
||||
by {authorId} was {mergedAt ? `merged` : `opened`}{' '}
|
||||
<span id={`date-${prNumber}`}>
|
||||
{formatIntoRelativeDate(mergedAt ? mergedAt : createdAt)}
|
||||
</span>
|
||||
<StyledTooltip
|
||||
anchorSelect={`#date-${prNumber}`}
|
||||
content={format(
|
||||
new Date(mergedAt ? mergedAt : createdAt),
|
||||
'dd MMMM yyyy',
|
||||
)}
|
||||
clickable
|
||||
noArrow
|
||||
/>
|
||||
</StyledDescription>
|
||||
</div>
|
||||
</Item>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CardContainer } from '@/app/_components/contributors/CardContainer';
|
||||
import { PullRequestItem } from '@/app/_components/contributors/PullRequestItem';
|
||||
import { Title } from '@/app/_components/contributors/Title';
|
||||
|
||||
const List = styled.div`
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-direction: column;
|
||||
`;
|
||||
interface PullRequestsProps {
|
||||
list: {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
mergedAt: string | null;
|
||||
authorId: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const PullRequests = ({ list }: PullRequestsProps) => {
|
||||
return (
|
||||
<CardContainer>
|
||||
<Title>Pull Requests</Title>
|
||||
<List>
|
||||
{list.map((pr) => (
|
||||
<PullRequestItem
|
||||
key={pr.id}
|
||||
id={pr.id}
|
||||
title={pr.title}
|
||||
url={pr.url}
|
||||
createdAt={pr.createdAt}
|
||||
mergedAt={pr.mergedAt}
|
||||
authorId={pr.authorId}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CardContainer } from '@/app/_components/contributors/CardContainer';
|
||||
import { HeartIcon } from '@/app/_components/ui/icons/SvgIcons';
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
display: flex;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
gap: 8px;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeartIcon = styled(HeartIcon)`
|
||||
@media (max-width: 810px) {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ThankYouProps {
|
||||
authorId: string;
|
||||
}
|
||||
|
||||
export const ThankYou = ({ authorId }: ThankYouProps) => {
|
||||
return (
|
||||
<CardContainer>
|
||||
<StyledTitle>
|
||||
Thank you @{authorId} <StyledHeartIcon color="#333333" size="18px" />
|
||||
</StyledTitle>
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const Title = styled.h3`
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
line-height: 41.6px;
|
||||
font-weight: 500;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 24px;
|
||||
line-height: 31.2px;
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const BackgroundContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 200%;
|
||||
background-image: url(https://framerusercontent.com/images/nqEmdwe7yDXNsOZovuxG5zvj2E.png);
|
||||
background-size: auto 20px;
|
||||
background-repeat: repeat;
|
||||
transform-origin: center center;
|
||||
z-index: -2;
|
||||
`;
|
||||
|
||||
const Gradient = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 200%;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
185deg,
|
||||
#fff 8.33%,
|
||||
rgba(255, 255, 255, 0.08) 48.95%,
|
||||
#fff 92.18%
|
||||
);
|
||||
z-index: -1;
|
||||
`;
|
||||
|
||||
export const Background = () => {
|
||||
return (
|
||||
<>
|
||||
<BackgroundContainer />
|
||||
<Gradient />
|
||||
</>
|
||||
);
|
||||
};
|
||||
117
packages/twenty-website/src/app/_components/oss-friends/Card.tsx
Normal file
117
packages/twenty-website/src/app/_components/oss-friends/Card.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { Gabarito } from 'next/font/google';
|
||||
|
||||
export interface OssData {
|
||||
name: string;
|
||||
description: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 312px;
|
||||
height: 360px;
|
||||
background-color: #fafafa;
|
||||
border: 2px solid black;
|
||||
text-align: left;
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: 200ms;
|
||||
|
||||
&:hover {
|
||||
-webkit-box-shadow: -3px 3px 2px 0px rgba(0, 0, 0, 1);
|
||||
-moz-box-shadow: -3px 3px 2px 0px rgba(0, 0, 0, 1);
|
||||
box-shadow: -3px 3px 2px 0px rgba(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
width: 45%;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
@media (max-width: 810px) {
|
||||
width: 95%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.p`
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
`;
|
||||
|
||||
const Description = styled.p`
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #b3b3b3;
|
||||
`;
|
||||
|
||||
const Button = styled.a`
|
||||
border: 2px solid black;
|
||||
border-radius: 12px;
|
||||
padding: 16px 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
height: 60px;
|
||||
|
||||
&:after {
|
||||
content: 'Visit Website';
|
||||
border: 2px solid black;
|
||||
border-radius: 12px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 8px;
|
||||
bottom: 7px;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
left: 4px;
|
||||
bottom: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
const gabarito = Gabarito({
|
||||
weight: ['400', '500'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const Icon = styled.img`
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
// remove the protocol from the url
|
||||
const removeProtocol = (url: string) => url.replace(/(^\w+:|^)\/\//, '');
|
||||
|
||||
export const Card = ({ data }: { data: OssData }) => {
|
||||
return (
|
||||
<Container>
|
||||
<div>
|
||||
<Icon src={`https://favicon.twenty.com/${removeProtocol(data.href)}`} />
|
||||
<Title>{data.name}</Title>
|
||||
<Description>{data.description}</Description>
|
||||
</div>
|
||||
|
||||
<Button href={data.href} className={gabarito.className}></Button>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 32px;
|
||||
justify-content: center;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
export const CardContainer = ({ children }: { children?: React.ReactNode }) => {
|
||||
return <Container>{children}</Container>;
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
gap: 26px;
|
||||
@media (max-width: 809px) {
|
||||
width: 100%;
|
||||
padding: 0px 12px 0px 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContentContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return <Container>{children}</Container>;
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const Title = styled.h2`
|
||||
font-size: 56px;
|
||||
font-weight: 600;
|
||||
color: #b3b3b3;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 64px;
|
||||
|
||||
@media (max-width: 810px) {
|
||||
font-size: 28px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Description = styled.h2`
|
||||
font-size: 20px;
|
||||
color: #818181;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 36px;
|
||||
font-weight: 400;
|
||||
@media (max-width: 810px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
Open-source <br /> <span style={{ color: 'black' }}>friends</span>
|
||||
</Title>
|
||||
|
||||
<Description>
|
||||
We are proud to collaborate with a diverse group of partners to <br />
|
||||
promote open-source software.
|
||||
</Description>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,171 @@
|
||||
const getSize = (size: string) => {
|
||||
switch (size) {
|
||||
case 'S':
|
||||
return '14px';
|
||||
case 'M':
|
||||
return '24px';
|
||||
case 'L':
|
||||
return '48px';
|
||||
default:
|
||||
return size;
|
||||
}
|
||||
};
|
||||
|
||||
export const GithubIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
|
||||
const dimension = getSize(size);
|
||||
return (
|
||||
<div style={{ width: dimension, height: dimension }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14">
|
||||
<path
|
||||
d="M 6.979 0 C 3.12 0 0 3.143 0 7.031 C 0 10.139 1.999 12.77 4.772 13.701 C 5.119 13.771 5.246 13.55 5.246 13.364 C 5.246 13.201 5.234 12.642 5.234 12.06 C 3.293 12.479 2.889 11.222 2.889 11.222 C 2.577 10.407 2.114 10.197 2.114 10.197 C 1.479 9.767 2.161 9.767 2.161 9.767 C 2.866 9.813 3.235 10.488 3.235 10.488 C 3.859 11.559 4.865 11.257 5.269 11.07 C 5.327 10.616 5.512 10.302 5.708 10.127 C 4.16 9.964 2.531 9.359 2.531 6.658 C 2.531 5.89 2.808 5.262 3.247 4.773 C 3.178 4.598 2.935 3.876 3.316 2.91 C 3.316 2.91 3.906 2.724 5.234 3.632 C 5.803 3.478 6.39 3.4 6.979 3.399 C 7.568 3.399 8.169 3.481 8.724 3.632 C 10.053 2.724 10.642 2.91 10.642 2.91 C 11.023 3.876 10.781 4.598 10.711 4.773 C 11.162 5.262 11.428 5.89 11.428 6.658 C 11.428 9.359 9.799 9.953 8.239 10.127 C 8.493 10.349 8.712 10.768 8.712 11.431 C 8.712 12.374 8.701 13.131 8.701 13.363 C 8.701 13.55 8.828 13.771 9.175 13.701 C 11.948 12.77 13.947 10.139 13.947 7.031 C 13.958 3.143 10.827 0 6.979 0 Z"
|
||||
fill={color}
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LinkedInIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
|
||||
const dimension = getSize(size);
|
||||
|
||||
return (
|
||||
<div style={{ width: dimension, height: dimension }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
focusable="false"
|
||||
color={color}
|
||||
>
|
||||
<g color={color}>
|
||||
<path
|
||||
d="M216,24H40A16,16,0,0,0,24,40V216a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V40A16,16,0,0,0,216,24Zm0,192H40V40H216V216ZM96,112v64a8,8,0,0,1-16,0V112a8,8,0,0,1,16,0Zm88,28v36a8,8,0,0,1-16,0V140a20,20,0,0,0-40,0v36a8,8,0,0,1-16,0V112a8,8,0,0,1,15.79-1.78A36,36,0,0,1,184,140ZM100,84A12,12,0,1,1,88,72,12,12,0,0,1,100,84Z"
|
||||
fill={color}
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DiscordIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
|
||||
const dimension = getSize(size);
|
||||
return (
|
||||
<div style={{ width: dimension, height: dimension }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
focusable="false"
|
||||
color={color}
|
||||
>
|
||||
<g color={color}>
|
||||
<path
|
||||
d="M104,140a12,12,0,1,1-12-12A12,12,0,0,1,104,140Zm60-12a12,12,0,1,0,12,12A12,12,0,0,0,164,128Zm74.45,64.9-67,29.71a16.17,16.17,0,0,1-21.71-9.1l-8.11-22q-6.72.45-13.63.46t-13.63-.46l-8.11,22a16.18,16.18,0,0,1-21.71,9.1l-67-29.71a15.93,15.93,0,0,1-9.06-18.51L38,58A16.07,16.07,0,0,1,51,46.14l36.06-5.93a16.22,16.22,0,0,1,18.26,11.88l3.26,12.84Q118.11,64,128,64t19.4.93l3.26-12.84a16.21,16.21,0,0,1,18.26-11.88L205,46.14A16.07,16.07,0,0,1,218,58l29.53,116.38A15.93,15.93,0,0,1,238.45,192.9ZM232,178.28,202.47,62s0,0-.08,0L166.33,56a.17.17,0,0,0-.17,0l-2.83,11.14c5,.94,10,2.06,14.83,3.42A8,8,0,0,1,176,86.31a8.09,8.09,0,0,1-2.16-.3A172.25,172.25,0,0,0,128,80a172.25,172.25,0,0,0-45.84,6,8,8,0,1,1-4.32-15.4c4.82-1.36,9.78-2.48,14.82-3.42L89.83,56s0,0-.12,0h0L53.61,61.93a.17.17,0,0,0-.09,0L24,178.33,91,208a.23.23,0,0,0,.22,0L98,189.72a173.2,173.2,0,0,1-20.14-4.32A8,8,0,0,1,82.16,170,171.85,171.85,0,0,0,128,176a171.85,171.85,0,0,0,45.84-6,8,8,0,0,1,4.32,15.41A173.2,173.2,0,0,1,158,189.72L164.75,208a.22.22,0,0,0,.21,0Z"
|
||||
fill={color}
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const XIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
|
||||
const dimension = getSize(size);
|
||||
return (
|
||||
<div style={{ width: dimension, height: dimension }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
id="svg2382164700"
|
||||
>
|
||||
<path
|
||||
d="M 15.418 19.037 L 3.44 3.637 C 3.311 3.471 3.288 3.247 3.381 3.058 C 3.473 2.87 3.665 2.75 3.875 2.75 L 6.148 2.75 C 6.318 2.75 6.478 2.829 6.582 2.963 L 18.56 18.363 C 18.689 18.529 18.712 18.753 18.619 18.942 C 18.527 19.13 18.335 19.25 18.125 19.25 L 15.852 19.25 C 15.682 19.25 15.522 19.171 15.418 19.037 Z"
|
||||
fill="transparent"
|
||||
strokeWidth="1.38"
|
||||
strokeMiterlimit="10"
|
||||
stroke={color}
|
||||
></path>
|
||||
<path
|
||||
d="M 18.333 2.75 L 3.667 19.25"
|
||||
fill="transparent"
|
||||
strokeWidth="1.38"
|
||||
strokeLinecap="round"
|
||||
strokeMiterlimit="10"
|
||||
stroke={color}
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubIcon2 = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
|
||||
const dimension = getSize(size);
|
||||
return (
|
||||
<div style={{ width: dimension, height: dimension }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
focusable="false"
|
||||
color={color}
|
||||
>
|
||||
<g color={color}>
|
||||
<path
|
||||
d="M208.31,75.68A59.78,59.78,0,0,0,202.93,28,8,8,0,0,0,196,24a59.75,59.75,0,0,0-48,24H124A59.75,59.75,0,0,0,76,24a8,8,0,0,0-6.93,4,59.78,59.78,0,0,0-5.38,47.68A58.14,58.14,0,0,0,56,104v8a56.06,56.06,0,0,0,48.44,55.47A39.8,39.8,0,0,0,96,192v8H72a24,24,0,0,1-24-24A40,40,0,0,0,8,136a8,8,0,0,0,0,16,24,24,0,0,1,24,24,40,40,0,0,0,40,40H96v16a8,8,0,0,0,16,0V192a24,24,0,0,1,48,0v40a8,8,0,0,0,16,0V192a39.8,39.8,0,0,0-8.44-24.53A56.06,56.06,0,0,0,216,112v-8A58.14,58.14,0,0,0,208.31,75.68ZM200,112a40,40,0,0,1-40,40H112a40,40,0,0,1-40-40v-8a41.74,41.74,0,0,1,6.9-22.48A8,8,0,0,0,80,73.83a43.81,43.81,0,0,1,.79-33.58,43.88,43.88,0,0,1,32.32,20.06A8,8,0,0,0,119.82,64h32.35a8,8,0,0,0,6.74-3.69,43.87,43.87,0,0,1,32.32-20.06A43.81,43.81,0,0,1,192,73.83a8.09,8.09,0,0,0,1,7.65A41.72,41.72,0,0,1,200,104Z"
|
||||
fill={color}
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PullRequestIcon = ({ size = 'S', color = 'rgb(179,179,179)' }) => {
|
||||
const dimension = getSize(size);
|
||||
return (
|
||||
<div style={{ width: dimension, height: dimension }}>
|
||||
<svg viewBox="0 0 18 19" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 14C3.53043 14 4.03914 14.2107 4.41421 14.5858C4.78929 14.9609 5 15.4696 5 16C5 16.5304 4.78929 17.0391 4.41421 17.4142C4.03914 17.7893 3.53043 18 3 18C2.46957 18 1.96086 17.7893 1.58579 17.4142C1.21071 17.0391 1 16.5304 1 16C1 15.4696 1.21071 14.9609 1.58579 14.5858C1.96086 14.2107 2.46957 14 3 14ZM3 14V6M3 6C2.46957 6 1.96086 5.78929 1.58579 5.41421C1.21071 5.03914 1 4.53043 1 4C1 3.46957 1.21071 2.96086 1.58579 2.58579C1.96086 2.21071 2.46957 2 3 2C3.53043 2 4.03914 2.21071 4.41421 2.58579C4.78929 2.96086 5 3.46957 5 4C5 4.53043 4.78929 5.03914 4.41421 5.41421C4.03914 5.78929 3.53043 6 3 6ZM15 14C15.5304 14 16.0391 14.2107 16.4142 14.5858C16.7893 14.9609 17 15.4696 17 16C17 16.5304 16.7893 17.0391 16.4142 17.4142C16.0391 17.7893 15.5304 18 15 18C14.4696 18 13.9609 17.7893 13.5858 17.4142C13.2107 17.0391 13 16.5304 13 16C13 15.4696 13.2107 14.9609 13.5858 14.5858C13.9609 14.2107 14.4696 14 15 14ZM15 14V6C15 5.46957 14.7893 4.96086 14.4142 4.58579C14.0391 4.21071 13.5304 4 13 4H8M8 4L11 7M8 4L11 1"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="transparent"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const HeartIcon = ({ size = 'S', color = 'rgb(179,179,179)' }) => {
|
||||
const dimension = getSize(size);
|
||||
return (
|
||||
<div style={{ width: dimension, height: dimension }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 20" fill="none">
|
||||
<path
|
||||
d="M18.513 10.572L11.013 18L3.513 10.572C3.0183 10.0906 2.62864 9.51201 2.36854 8.87263C2.10845 8.23325 1.98356 7.54694 2.00173 6.85693C2.01991 6.16691 2.18076 5.48813 2.47415 4.86333C2.76755 4.23853 3.18713 3.68125 3.70648 3.22657C4.22583 2.7719 4.8337 2.42968 5.49181 2.22147C6.14991 2.01327 6.844 1.94358 7.53036 2.0168C8.21673 2.09001 8.8805 2.30455 9.47987 2.6469C10.0792 2.98925 10.6012 3.45199 11.013 4.00599C11.4265 3.45602 11.9491 2.99731 12.5481 2.6586C13.1471 2.31988 13.8095 2.10844 14.4939 2.03751C15.1784 1.96658 15.8701 2.03769 16.5258 2.24639C17.1815 2.45508 17.787 2.79687 18.3045 3.25036C18.8221 3.70385 19.2404 4.25928 19.5334 4.88189C19.8264 5.50449 19.9877 6.18088 20.0073 6.8687C20.0269 7.55653 19.9043 8.24099 19.6471 8.87924C19.39 9.5175 19.0039 10.0958 18.513 10.578"
|
||||
stroke={color}
|
||||
strokeWidth="2.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExternalArrow = () => {
|
||||
return (
|
||||
<div style={{ width: '14px', height: '14px', fill: 'rgb(179, 179, 179)' }}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
focusable="false"
|
||||
color="rgb(179, 179, 179)"
|
||||
>
|
||||
<g color="rgb(179, 179, 179)">
|
||||
<path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
export type { TablerIconsProps } from '@tabler/icons-react';
|
||||
export {
|
||||
IconBook,
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
} from '@tabler/icons-react';
|
||||
@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronLeft } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
DeviceType,
|
||||
useDeviceType,
|
||||
} from '@/app/_components/client-utils/useDeviceType';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
gap: ${Theme.spacing(2)};
|
||||
color: #b3b3b3;
|
||||
`;
|
||||
|
||||
const InternalLinkItem = styled(Link)`
|
||||
text-decoration: none;
|
||||
color: #b3b3b3;
|
||||
&:hover {
|
||||
color: ${Theme.text.color.quarternary};
|
||||
}
|
||||
`;
|
||||
|
||||
const ExternalLinkItem = styled.a`
|
||||
text-decoration: none;
|
||||
color: #b3b3b3;
|
||||
`;
|
||||
|
||||
const ActivePage = styled.span`
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledSection = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${Theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledMobileContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(1)};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
font-size: ${Theme.font.size.sm};
|
||||
`;
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
items: {
|
||||
uri: string;
|
||||
label: string;
|
||||
isExternal?: boolean;
|
||||
}[];
|
||||
activePage: string;
|
||||
separator: string;
|
||||
}
|
||||
|
||||
export const Breadcrumbs = ({
|
||||
items,
|
||||
activePage,
|
||||
separator,
|
||||
}: BreadcrumbsProps) => {
|
||||
const isMobile = useDeviceType() === DeviceType.MOBILE;
|
||||
if (isMobile) {
|
||||
const lastItem = items[items.length - 1];
|
||||
return (
|
||||
<StyledMobileContainer>
|
||||
<IconChevronLeft size={Theme.icon.size.md} />
|
||||
<InternalLinkItem href={lastItem.uri}>
|
||||
{lastItem.label}
|
||||
</InternalLinkItem>
|
||||
</StyledMobileContainer>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
{items.map((item, index) => (
|
||||
<StyledSection key={`${item?.uri ?? 'item'}-${index}`}>
|
||||
{item.isExternal ? (
|
||||
<ExternalLinkItem href={item.uri}>{item.label}</ExternalLinkItem>
|
||||
) : (
|
||||
<InternalLinkItem href={item.uri}>{item.label}</InternalLinkItem>
|
||||
)}
|
||||
<div>{separator}</div>
|
||||
</StyledSection>
|
||||
))}
|
||||
<ActivePage>{activePage}</ActivePage>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 0px 96px 0px 96px;
|
||||
@media (max-width: 809px) {
|
||||
width: 100%;
|
||||
padding: 0px 12px 0px 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContentContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
return <Container>{children}</Container>;
|
||||
};
|
||||
@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import {
|
||||
DiscordIcon,
|
||||
GithubIcon2,
|
||||
LinkedInIcon,
|
||||
XIcon,
|
||||
} from '../icons/SvgIcons';
|
||||
|
||||
import { Logo } from './Logo';
|
||||
|
||||
const FooterContainer = styled.div`
|
||||
padding: 64px 96px 64px 96px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: rgb(129, 129, 129);
|
||||
gap: 32px;
|
||||
@media (max-width: 809px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LeftSideFooter = styled.div`
|
||||
width: 36Opx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const RightSideFooter = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 48px;
|
||||
height: 146px;
|
||||
`;
|
||||
|
||||
const RightSideFooterColumn = styled.div`
|
||||
width: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const RightSideFooterLink = styled.a`
|
||||
color: rgb(129, 129, 129);
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: #000;
|
||||
}
|
||||
`;
|
||||
|
||||
const RightSideFooterColumnTitle = styled.div`
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
`;
|
||||
|
||||
export const FooterDesktop = () => {
|
||||
const path = usePathname();
|
||||
const isTwentyDev = path.includes('developers');
|
||||
|
||||
if (isTwentyDev) return;
|
||||
|
||||
return (
|
||||
<FooterContainer>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<LeftSideFooter>
|
||||
<Logo />
|
||||
<div>The #1 Open Source CRM</div>
|
||||
</LeftSideFooter>
|
||||
<RightSideFooter>
|
||||
<RightSideFooterColumn>
|
||||
<RightSideFooterColumnTitle>Company</RightSideFooterColumnTitle>
|
||||
<RightSideFooterLink href="/pricing">Pricing</RightSideFooterLink>
|
||||
<RightSideFooterLink href="/story">Story</RightSideFooterLink>
|
||||
</RightSideFooterColumn>
|
||||
<RightSideFooterColumn>
|
||||
<RightSideFooterColumnTitle>Resources</RightSideFooterColumnTitle>
|
||||
<RightSideFooterLink href="https://docs.twenty.com">
|
||||
Documentation
|
||||
</RightSideFooterLink>
|
||||
<RightSideFooterLink href="/releases">
|
||||
Changelog
|
||||
</RightSideFooterLink>
|
||||
</RightSideFooterColumn>
|
||||
<RightSideFooterColumn>
|
||||
<RightSideFooterColumnTitle>Other</RightSideFooterColumnTitle>
|
||||
<RightSideFooterLink href="/oss-friends">
|
||||
OSS Friends
|
||||
</RightSideFooterLink>
|
||||
<RightSideFooterLink href="/legal/terms">
|
||||
Terms of Service
|
||||
</RightSideFooterLink>
|
||||
<RightSideFooterLink href="/legal/privacy">
|
||||
Privacy Policy
|
||||
</RightSideFooterLink>
|
||||
</RightSideFooterColumn>
|
||||
</RightSideFooter>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
borderTop: '1px solid rgb(179, 179, 179)',
|
||||
paddingTop: '32px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>©</span>
|
||||
2023 Twenty PBC
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<a href="https://x.com/twentycrm" target="_blank" rel="noreferrer">
|
||||
<XIcon size="M" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/twentyhq/twenty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<GithubIcon2 size="M" />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/company/twenty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<LinkedInIcon size="M" />
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/UfGNZJfAG6"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<DiscordIcon size="M" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</FooterContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,123 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ExternalArrow } from '@/app/_components/ui/icons/SvgIcons';
|
||||
|
||||
import { GithubIcon } from '../icons/SvgIcons';
|
||||
|
||||
import { Logo } from './Logo';
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
padding: 12px 16px 12px 16px;
|
||||
position: relative;
|
||||
transform-origin: 50% 50% 0px;
|
||||
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
|
||||
|
||||
@media (max-width: 809px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
const ListItem = styled.a`
|
||||
color: rgb(71, 71, 71);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
height: 40px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
&:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
`;
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 202px;
|
||||
`;
|
||||
|
||||
const StyledButton = styled.div`
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
align-items: center;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
outline: inherit;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const CallToActionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkNextToCTA = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(71, 71, 71);
|
||||
padding: 0px 16px 0px 16px;
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const CallToAction = () => {
|
||||
return (
|
||||
<CallToActionContainer>
|
||||
<LinkNextToCTA href="https://github.com/twentyhq/twenty">
|
||||
Sign in
|
||||
</LinkNextToCTA>
|
||||
<a href="https://twenty.com/stripe-redirection">
|
||||
<StyledButton>Get Started</StyledButton>
|
||||
</a>
|
||||
</CallToActionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const HeaderDesktop = () => {
|
||||
return (
|
||||
<Nav>
|
||||
<LogoContainer>
|
||||
<Logo />
|
||||
</LogoContainer>
|
||||
<LinkList>
|
||||
<ListItem href="/pricing">Pricing</ListItem>
|
||||
<ListItem href="/story">Story</ListItem>
|
||||
<ListItem href="https://docs.twenty.com">
|
||||
Docs <ExternalArrow />
|
||||
</ListItem>
|
||||
<ListItem href="https://github.com/twentyhq/twenty">
|
||||
<GithubIcon color="rgb(71,71,71)" /> 8.3k <ExternalArrow />
|
||||
</ListItem>
|
||||
</LinkList>
|
||||
<CallToAction />
|
||||
</Nav>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,235 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IBM_Plex_Mono } from 'next/font/google';
|
||||
|
||||
import { ExternalArrow } from '@/app/_components/ui/icons/SvgIcons';
|
||||
|
||||
import { GithubIcon } from '../icons/SvgIcons';
|
||||
|
||||
import { Logo } from './Logo';
|
||||
|
||||
const IBMPlexMono = IBM_Plex_Mono({
|
||||
weight: '500',
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
transform-origin: 50% 50% 0px;
|
||||
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
|
||||
height: 64px;
|
||||
width: 100%;
|
||||
@media (min-width: 810px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ListItem = styled.a`
|
||||
color: rgb(71, 71, 71);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
height: 48px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
&:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
`;
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 202px;
|
||||
`;
|
||||
|
||||
const LogoAddon = styled.div`
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 150%;
|
||||
`;
|
||||
|
||||
const StyledButton = styled.div`
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
align-items: center;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
outline: inherit;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const CallToActionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkNextToCTA = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(71, 71, 71);
|
||||
padding: 0px 16px 0px 16px;
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const CallToAction = () => {
|
||||
return (
|
||||
<CallToActionContainer>
|
||||
<LinkNextToCTA href="https://github.com/twentyhq/twenty">
|
||||
Sign in
|
||||
</LinkNextToCTA>
|
||||
<a href="#">
|
||||
<StyledButton>Get Started</StyledButton>
|
||||
</a>
|
||||
</CallToActionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const HamburgerContainer = styled.div`
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
input {
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#line1 {
|
||||
transition: transform 0.5s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
#line2 {
|
||||
transition: transform 0.5s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
#menu-input:checked ~ #line1 {
|
||||
transform: rotate(45deg) translate(7px);
|
||||
}
|
||||
|
||||
#menu-input:checked ~ #line2 {
|
||||
transform: rotate(-45deg) translate(7px);
|
||||
}
|
||||
`;
|
||||
|
||||
const HamburgerLine1 = styled.div`
|
||||
height: 2px;
|
||||
left: calc(50.00000000000002% - 20px / 2);
|
||||
position: absolute;
|
||||
top: calc(37.50000000000002% - 2px / 2);
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(179, 179, 179);
|
||||
`;
|
||||
|
||||
const HamburgerLine2 = styled.div`
|
||||
height: 2px;
|
||||
left: calc(50.00000000000002% - 20px / 2);
|
||||
position: absolute;
|
||||
top: calc(62.50000000000002% - 2px / 2);
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(179, 179, 179);
|
||||
`;
|
||||
|
||||
const NavOpen = styled.div`
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
inset: 0px;
|
||||
top: 63px;
|
||||
background-color: #fff;
|
||||
gap: 33px;
|
||||
padding-top: 32px;
|
||||
z-index: 100;
|
||||
transition: transform 0.2s ease-in;
|
||||
display: flex;
|
||||
transform-origin: top;
|
||||
`;
|
||||
|
||||
const MobileMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const HeaderMobile = () => {
|
||||
const isTwentyDev = false;
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const toggleMenu = () => {
|
||||
setMenuOpen(!menuOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<MobileMenu>
|
||||
<Nav>
|
||||
<LogoContainer>
|
||||
<Logo />
|
||||
{isTwentyDev && (
|
||||
<LogoAddon className={IBMPlexMono.className}>
|
||||
for Developers
|
||||
</LogoAddon>
|
||||
)}
|
||||
</LogoContainer>
|
||||
<HamburgerContainer>
|
||||
<input type="checkbox" id="menu-input" onChange={toggleMenu} />
|
||||
<HamburgerLine1 id="line1" />
|
||||
<HamburgerLine2 id="line2" />
|
||||
</HamburgerContainer>
|
||||
</Nav>
|
||||
<NavOpen
|
||||
style={{
|
||||
transform: `scaleY(${menuOpen ? '1' : '0'})`,
|
||||
}}
|
||||
>
|
||||
<LinkList>
|
||||
<ListItem href="/story">Story</ListItem>
|
||||
<ListItem href="/pricing">Pricing</ListItem>
|
||||
<ListItem href="https://docs.twenty.com">
|
||||
Docs <ExternalArrow />
|
||||
</ListItem>
|
||||
<ListItem href="https://github.com/twentyhq/twenty">
|
||||
<GithubIcon color="rgb(71,71,71)" /> 8.3k <ExternalArrow />
|
||||
</ListItem>
|
||||
</LinkList>
|
||||
<CallToAction />
|
||||
</NavOpen>
|
||||
</MobileMenu>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const Link = styled.a`
|
||||
display: block;
|
||||
image-rendering: pixelated;
|
||||
flex-shrink: 0;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 8px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background-image: url('/images/core/logo.svg');
|
||||
opacity: 1;
|
||||
`;
|
||||
|
||||
export const Logo = () => {
|
||||
return <Link href="/" />;
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
export const PostImage = ({
|
||||
sources,
|
||||
style,
|
||||
}: {
|
||||
sources: { light: string; dark: string };
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
return <Image src={sources.light} style={style} alt={sources.light} />;
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { Color, rgba } from './colors';
|
||||
|
||||
export const Background = {
|
||||
primary: Color.white,
|
||||
secondary: Color.gray10,
|
||||
tertiary: Color.gray20,
|
||||
transparent: {
|
||||
strong: rgba(Color.gray60, 0.16),
|
||||
medium: rgba(Color.gray60, 0.08),
|
||||
light: rgba(Color.gray60, 0.06),
|
||||
lighter: rgba(Color.gray60, 0.04),
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { Color } from './colors';
|
||||
|
||||
const common = {
|
||||
radius: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
xl: '20px',
|
||||
pill: '999px',
|
||||
rounded: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
export const Border = {
|
||||
color: {
|
||||
plain: Color.gray60,
|
||||
},
|
||||
...common,
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
import hexRgb from 'hex-rgb';
|
||||
|
||||
export const mainColors = {
|
||||
white: '#ffffff',
|
||||
};
|
||||
|
||||
export const secondaryColors = {
|
||||
gray60: '#141414',
|
||||
gray50: '#474747',
|
||||
gray40: '#818181',
|
||||
gray30: '#b3b3b3',
|
||||
gray20: '#f1f1f1',
|
||||
gray10: '#fafafa',
|
||||
};
|
||||
|
||||
export const Color = {
|
||||
...mainColors,
|
||||
...secondaryColors,
|
||||
};
|
||||
|
||||
export const rgba = (hex: string, alpha: number) => {
|
||||
const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(',');
|
||||
return `rgba(${rgb},${alpha})`;
|
||||
};
|
||||
14
packages/twenty-website/src/app/_components/ui/theme/font.ts
Normal file
14
packages/twenty-website/src/app/_components/ui/theme/font.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const Font = {
|
||||
size: {
|
||||
xs: '0.875rem',
|
||||
sm: '1rem',
|
||||
base: '1.125rem',
|
||||
lg: '1.25rem',
|
||||
xl: '1.5rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
},
|
||||
family: 'Inter, sans-serif',
|
||||
};
|
||||
13
packages/twenty-website/src/app/_components/ui/theme/icon.ts
Normal file
13
packages/twenty-website/src/app/_components/ui/theme/icon.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const Icon = {
|
||||
size: {
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 20,
|
||||
xl: 40,
|
||||
},
|
||||
stroke: {
|
||||
sm: 1.6,
|
||||
md: 2,
|
||||
lg: 2.5,
|
||||
},
|
||||
};
|
||||
22
packages/twenty-website/src/app/_components/ui/theme/text.ts
Normal file
22
packages/twenty-website/src/app/_components/ui/theme/text.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Color } from './colors';
|
||||
|
||||
export const Text = {
|
||||
color: {
|
||||
primary: Color.gray60,
|
||||
secondary: Color.gray50,
|
||||
tertiary: Color.gray40,
|
||||
quarternary: Color.gray30,
|
||||
Inverted: Color.white,
|
||||
},
|
||||
lineHeight: {
|
||||
lg: 1.5,
|
||||
md: 1.2,
|
||||
},
|
||||
|
||||
iconSizeMedium: 16,
|
||||
iconSizeSmall: 14,
|
||||
|
||||
iconStrikeLight: 1.6,
|
||||
iconStrikeMedium: 2,
|
||||
iconStrikeBold: 2.5,
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { Background } from '@/app/_components/ui/theme/background';
|
||||
import { Border } from '@/app/_components/ui/theme/border';
|
||||
import { Color } from '@/app/_components/ui/theme/colors';
|
||||
import { Font } from '@/app/_components/ui/theme/font';
|
||||
import { Icon } from '@/app/_components/ui/theme/icon';
|
||||
import { Text } from '@/app/_components/ui/theme/text';
|
||||
|
||||
export const Theme = {
|
||||
color: Color,
|
||||
border: Border,
|
||||
background: Background,
|
||||
text: Text,
|
||||
spacingMultiplicator: 4,
|
||||
icon: Icon,
|
||||
font: Font,
|
||||
spacing: (...args: number[]) =>
|
||||
args.map((multiplicator) => `${multiplicator * 4}px`).join(' '),
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { UserGuideHomeCardsType } from '@/content/user-guide/constants/UserGuideHomeCards';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
color: ${Theme.border.color.plain};
|
||||
border: 2px solid ${Theme.border.color.plain};
|
||||
border-radius: ${Theme.border.radius.md};
|
||||
padding: ${Theme.spacing(6)};
|
||||
gap: ${Theme.spacing(4)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
width: 348px;
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
font-size: ${Theme.font.size.lg};
|
||||
color: ${Theme.text.color.primary};
|
||||
`;
|
||||
|
||||
const StyledSubHeading = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-family: ${Theme.font.family};
|
||||
`;
|
||||
|
||||
const StyledImage = styled.img`
|
||||
width: 300px;
|
||||
`;
|
||||
|
||||
export default function UserGuideCard({
|
||||
card,
|
||||
}: {
|
||||
card: UserGuideHomeCardsType;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<StyledContainer onClick={() => router.push(`/user-guide/${card.url}`)}>
|
||||
<StyledImage src={card.image} alt={card.title} />
|
||||
<StyledHeading>{card.title}</StyledHeading>
|
||||
<StyledSubHeading>{card.subtitle}</StyledSubHeading>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
DeviceType,
|
||||
useDeviceType,
|
||||
} from '@/app/_components/client-utils/useDeviceType';
|
||||
import { Breadcrumbs } from '@/app/_components/ui/layout/Breadcrumbs';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { FileContent } from '@/app/_server-utils/get-posts';
|
||||
|
||||
const StyledContainer = styled.div<{ devicetype: string }>`
|
||||
width: ${({ devicetype }) =>
|
||||
devicetype === DeviceType.TABLET
|
||||
? '70%'
|
||||
: devicetype === DeviceType.DESKTOP
|
||||
? '60%'
|
||||
: '100%'};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-family: ${Theme.font.family};
|
||||
border-bottom: 1px solid ${Theme.background.transparent.medium};
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 79.3%;
|
||||
padding: ${Theme.spacing(10)} 0px ${Theme.spacing(20)} 0px;
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSectionTitle = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
padding: ${Theme.spacing(2)} 0px;
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSectionSub = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(4)};
|
||||
color: ${Theme.text.color.tertiary};
|
||||
font-family: ${Theme.font.family};
|
||||
`;
|
||||
|
||||
const StyledRectangle = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: ${Theme.background.transparent.medium};
|
||||
`;
|
||||
|
||||
export default function UserGuideContent({ item }: { item: FileContent }) {
|
||||
const BREADCRUMB_ITEMS = [
|
||||
{
|
||||
uri: '/user-guide',
|
||||
label: 'User Guide',
|
||||
},
|
||||
];
|
||||
const deviceType = useDeviceType();
|
||||
return (
|
||||
<StyledContainer devicetype={deviceType}>
|
||||
<StyledWrapper>
|
||||
<StyledHeader>
|
||||
<Breadcrumbs
|
||||
items={BREADCRUMB_ITEMS}
|
||||
activePage={item.itemInfo.title}
|
||||
separator="/"
|
||||
/>
|
||||
<StyledHeading>{item.itemInfo.title}</StyledHeading>
|
||||
{item.itemInfo.image && (
|
||||
<img
|
||||
id={`img-${item.itemInfo.title}`}
|
||||
src={item.itemInfo.image}
|
||||
alt={item.itemInfo.title}
|
||||
/>
|
||||
)}
|
||||
<StyledHeaderInfoSection>
|
||||
<StyledHeaderInfoSectionTitle>
|
||||
In this article
|
||||
</StyledHeaderInfoSectionTitle>
|
||||
<StyledHeaderInfoSectionSub>
|
||||
{item.itemInfo.info}
|
||||
</StyledHeaderInfoSectionSub>
|
||||
</StyledHeaderInfoSection>
|
||||
<StyledRectangle />
|
||||
</StyledHeader>
|
||||
<div>{item.content}</div>
|
||||
</StyledWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
DeviceType,
|
||||
useDeviceType,
|
||||
} from '@/app/_components/client-utils/useDeviceType';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import UserGuideCard from '@/app/_components/user-guide/UserGuideCard';
|
||||
import { UserGuideHomeCards } from '@/content/user-guide/constants/UserGuideHomeCards';
|
||||
|
||||
const StyledContainer = styled.div<{ isMobile: boolean }>`
|
||||
width: ${({ isMobile }) => (isMobile ? '100%' : '60%')};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 79.3%;
|
||||
padding: ${Theme.spacing(10)} 0px ${Theme.spacing(20)} 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.h1`
|
||||
line-height: 38px;
|
||||
font-weight: 700;
|
||||
font-size: 38px;
|
||||
color: ${Theme.text.color.primary};
|
||||
margin: 0px;
|
||||
`;
|
||||
|
||||
const StyledSubHeading = styled.h1`
|
||||
line-height: 12px;
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.regular};
|
||||
color: ${Theme.text.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledContentGrid = styled.div`
|
||||
width: 100%;
|
||||
padding-top: ${Theme.spacing(6)};
|
||||
display: grid;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-columns: auto auto;
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledContentFlex = styled.div`
|
||||
width: 100%;
|
||||
padding-top: ${Theme.spacing(6)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
export default function UserGuideMain() {
|
||||
const deviceType = useDeviceType();
|
||||
return (
|
||||
<StyledContainer isMobile={deviceType === DeviceType.MOBILE}>
|
||||
<StyledWrapper>
|
||||
<StyledHeader>
|
||||
<StyledHeading>User Guide</StyledHeading>
|
||||
<StyledSubHeading>
|
||||
A brief guide to grasp the basics of Twenty
|
||||
</StyledSubHeading>
|
||||
</StyledHeader>
|
||||
{deviceType === DeviceType.DESKTOP ? (
|
||||
<StyledContentGrid>
|
||||
{UserGuideHomeCards.map((card) => {
|
||||
return <UserGuideCard key={card.title} card={card} />;
|
||||
})}
|
||||
</StyledContentGrid>
|
||||
) : (
|
||||
<StyledContentFlex>
|
||||
{UserGuideHomeCards.map((card) => {
|
||||
return <UserGuideCard key={card.title} card={card} />;
|
||||
})}
|
||||
</StyledContentFlex>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import {
|
||||
DeviceType,
|
||||
useDeviceType,
|
||||
} from '@/app/_components/client-utils/useDeviceType';
|
||||
import { IconBook } from '@/app/_components/ui/icons';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import UserGuideSidebarSection from '@/app/_components/user-guide/UserGuideSidebarSection';
|
||||
import { UserGuideIndex } from '@/content/user-guide/constants/UserGuideIndex';
|
||||
|
||||
const StyledContainer = styled.div<{ isTablet: boolean }>`
|
||||
width: ${({ isTablet }) => (isTablet ? '30%' : '20%')};
|
||||
background: ${Theme.background.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid ${Theme.background.transparent.medium};
|
||||
border-bottom: 1px solid ${Theme.background.transparent.medium};
|
||||
padding: ${Theme.spacing(10)} ${Theme.spacing(3)};
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(2)};
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
color: ${Theme.text.color.secondary};
|
||||
border: 1px solid ${Theme.text.color.secondary};
|
||||
border-radius: ${Theme.border.radius.sm};
|
||||
padding: ${Theme.spacing(1)} ${Theme.spacing(1)} ${Theme.spacing(1)}
|
||||
${Theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledHeadingText = styled.div`
|
||||
cursor: pointer;
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const UserGuideSidebar = () => {
|
||||
const router = useRouter();
|
||||
const isTablet = useDeviceType() === DeviceType.TABLET;
|
||||
return (
|
||||
<StyledContainer isTablet={isTablet}>
|
||||
<StyledHeading>
|
||||
<StyledIconContainer>
|
||||
<IconBook size={Theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
<StyledHeadingText onClick={() => router.push('/user-guide')}>
|
||||
User Guide
|
||||
</StyledHeadingText>
|
||||
</StyledHeading>
|
||||
{Object.entries(UserGuideIndex).map(([heading, subtopics]) => (
|
||||
<UserGuideSidebarSection
|
||||
key={heading}
|
||||
title={heading}
|
||||
subTopics={subtopics}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideSidebar;
|
||||
@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { IconChevronDown, IconChevronRight } from '@/app/_components/ui/icons';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { IndexSubtopic } from '@/content/user-guide/constants/UserGuideIndex';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(2)};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
padding-bottom: ${Theme.spacing(2)};
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.xs};
|
||||
`;
|
||||
|
||||
const StyledSubTopicItem = styled.div<{ isselected: boolean }>`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: ${Theme.spacing(8)};
|
||||
color: ${(props) =>
|
||||
props.isselected ? Theme.text.color.primary : Theme.text.color.secondary};
|
||||
font-weight: ${(props) =>
|
||||
props.isselected ? Theme.font.weight.medium : Theme.font.weight.regular};
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.xs};
|
||||
gap: 19px;
|
||||
padding: ${(props) =>
|
||||
props.isselected ? '6px 12px 6px 11px' : '0px 12px 0px 11px'};
|
||||
background: ${(props) =>
|
||||
props.isselected
|
||||
? Theme.background.transparent.light
|
||||
: Theme.background.secondary};
|
||||
border-radius: ${Theme.border.radius.md};
|
||||
text-decoration: none;
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:visited,
|
||||
&:link,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
padding: 0px 4px 0px 4px;
|
||||
`;
|
||||
|
||||
const StyledRectangle = styled.div<{ isselected: boolean }>`
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
background: ${(props) =>
|
||||
props.isselected
|
||||
? Theme.border.color.plain
|
||||
: Theme.background.transparent.light};
|
||||
`;
|
||||
|
||||
const UserGuideSidebarSection = ({
|
||||
title,
|
||||
subTopics,
|
||||
}: {
|
||||
title: string;
|
||||
subTopics: IndexSubtopic[];
|
||||
}) => {
|
||||
const [isUnfolded, setUnfoldedState] = useState(true);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTitle onClick={() => setUnfoldedState(!isUnfolded)}>
|
||||
{isUnfolded ? (
|
||||
<StyledIcon>
|
||||
<IconChevronDown size={Theme.icon.size.md} />
|
||||
</StyledIcon>
|
||||
) : (
|
||||
<StyledIcon>
|
||||
<IconChevronRight size={Theme.icon.size.md} />
|
||||
</StyledIcon>
|
||||
)}
|
||||
<div>{title}</div>
|
||||
</StyledTitle>
|
||||
{isUnfolded &&
|
||||
subTopics.map((subtopic, index) => {
|
||||
const isselected = pathname === `/user-guide/${subtopic.url}`;
|
||||
return (
|
||||
<StyledSubTopicItem
|
||||
key={index}
|
||||
isselected={isselected}
|
||||
onClick={() => router.push(`/user-guide/${subtopic.url}`)}
|
||||
>
|
||||
<StyledRectangle isselected={isselected} />
|
||||
{subtopic.title}
|
||||
</StyledSubTopicItem>
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideSidebarSection;
|
||||
@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 20%;
|
||||
background: ${Theme.background.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid ${Theme.background.transparent.medium};
|
||||
border-bottom: 1px solid ${Theme.background.transparent.medium};
|
||||
padding: ${Theme.spacing(10)} ${Theme.spacing(6)};
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
position: fixed;
|
||||
`;
|
||||
|
||||
const StyledHeadingText = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
`;
|
||||
|
||||
const UserGuideTableContents = () => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContent>
|
||||
<StyledHeadingText onClick={() => router.push('/user-guide')}>
|
||||
Table of Contents
|
||||
</StyledHeadingText>
|
||||
</StyledContent>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideTableContents;
|
||||
@ -0,0 +1,21 @@
|
||||
interface Heading {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const UserGuideTocComponent = ({ headings }: { headings: Heading[] }) => {
|
||||
return (
|
||||
<div>
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
{headings.map((heading, index) => (
|
||||
<li key={index}>
|
||||
<a href={`#${heading.id}`}>{heading.value}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideTocComponent;
|
||||
Reference in New Issue
Block a user