forked from cerc-io/snowballtools-base
[T-4907: style] Re-styling home page & sidebar component (#132)
* ⚡️ feat: create heading component * ♻️ refactor: move sidebar inside shared components * ⚡️ feat: add polymorphic prop type for heading component * 🎨 style: re-styling project list page * ♻️ refactor: remove `.env` * 🎨 style: set default font weight to normal for heading component
This commit is contained in:
parent
282001c317
commit
62734308fc
@ -1,7 +0,0 @@
|
|||||||
REACT_APP_SERVER_URL = 'http://localhost:8000'
|
|
||||||
|
|
||||||
REACT_APP_GITHUB_CLIENT_ID =
|
|
||||||
REACT_APP_GITHUB_PWA_TEMPLATE_REPO =
|
|
||||||
REACT_APP_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO =
|
|
||||||
|
|
||||||
REACT_APP_WALLET_CONNECT_ID =
|
|
@ -0,0 +1,7 @@
|
|||||||
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const headingTheme = tv({
|
||||||
|
base: ['text-elements-high-em', 'font-display', 'font-normal'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type HeadingVariants = VariantProps<typeof headingTheme>;
|
30
packages/frontend/src/components/shared/Heading/Heading.tsx
Normal file
30
packages/frontend/src/components/shared/Heading/Heading.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { headingTheme, type HeadingVariants } from './Heading.theme';
|
||||||
|
import { PolymorphicProps } from 'types/common';
|
||||||
|
|
||||||
|
export type HeadingElementType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||||
|
|
||||||
|
export type HeadingProps<Element extends HeadingElementType> =
|
||||||
|
PropsWithChildren<PolymorphicProps<Element, HeadingVariants>>;
|
||||||
|
|
||||||
|
export const Heading = <Element extends HeadingElementType>({
|
||||||
|
children,
|
||||||
|
as,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HeadingProps<Element>) => {
|
||||||
|
// Component is the element that will be rendered
|
||||||
|
const Component = as ?? 'h1';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
className={headingTheme({
|
||||||
|
className,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
1
packages/frontend/src/components/shared/Heading/index.ts
Normal file
1
packages/frontend/src/components/shared/Heading/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Heading';
|
@ -5,8 +5,8 @@ import { Organization } from 'gql-client';
|
|||||||
import { Option } from '@material-tailwind/react';
|
import { Option } from '@material-tailwind/react';
|
||||||
import { useDisconnect } from 'wagmi';
|
import { useDisconnect } from 'wagmi';
|
||||||
|
|
||||||
import { useGQLClient } from '../context/GQLClientContext';
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
import AsyncSelect from './shared/AsyncSelect';
|
import AsyncSelect from 'components/shared/AsyncSelect';
|
||||||
import {
|
import {
|
||||||
ChevronGrabberHorizontal,
|
ChevronGrabberHorizontal,
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
@ -14,10 +14,11 @@ import {
|
|||||||
LifeBuoyIcon,
|
LifeBuoyIcon,
|
||||||
QuestionMarkRoundIcon,
|
QuestionMarkRoundIcon,
|
||||||
SettingsSlidersIcon,
|
SettingsSlidersIcon,
|
||||||
} from './shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { Tabs } from 'components/shared/Tabs';
|
import { Tabs } from 'components/shared/Tabs';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
|
||||||
const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { orgSlug } = useParams();
|
const { orgSlug } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
@ -42,20 +43,20 @@ const Sidebar = () => {
|
|||||||
}, [disconnect, navigate]);
|
}, [disconnect, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full p-4 pt-10">
|
<div className="flex flex-col h-full px-6 py-8 gap-9">
|
||||||
<div className="grow">
|
{/* Logo */}
|
||||||
<Link to={`/${orgSlug}`}>
|
<Link to={`/${orgSlug}`}>
|
||||||
<div className="flex items-center space-x-3 mb-10 ml-2">
|
<div className="flex items-center gap-3 px-2">
|
||||||
<img
|
<img
|
||||||
src="/logo.svg"
|
src="/logo.svg"
|
||||||
alt="Snowball Logo"
|
alt="Snowball Logo"
|
||||||
className="h-8 w-8 rounded-lg"
|
className="h-10 w-10 rounded-lg"
|
||||||
/>
|
/>
|
||||||
<span className="text-2xl font-bold text-snowball-900">
|
<Heading className="text-[24px] font-semibold">Snowball</Heading>
|
||||||
Snowball
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
{/* Switch organization */}
|
||||||
|
<div className="flex flex-1 flex-col gap-4">
|
||||||
<AsyncSelect
|
<AsyncSelect
|
||||||
containerProps={{ className: 'h-14 border-none' }}
|
containerProps={{ className: 'h-14 border-none' }}
|
||||||
labelProps={{ className: 'before:border-none after:border-none' }}
|
labelProps={{ className: 'before:border-none after:border-none' }}
|
||||||
@ -82,7 +83,7 @@ const Sidebar = () => {
|
|||||||
)}
|
)}
|
||||||
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
|
arrow={<ChevronGrabberHorizontal className="h-4 w-4 text-gray-500" />}
|
||||||
>
|
>
|
||||||
{/* TODO: Show label organization and manage in option */}
|
{/* // TODO: Show label organization and manage in option */}
|
||||||
{organizations.map((org) => (
|
{organizations.map((org) => (
|
||||||
<Option key={org.id} value={org.slug}>
|
<Option key={org.id} value={org.slug}>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
@ -99,7 +100,7 @@ const Sidebar = () => {
|
|||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
</AsyncSelect>
|
</AsyncSelect>
|
||||||
<Tabs defaultValue="Projects" orientation="vertical" className="mt-10">
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
{[
|
{[
|
||||||
{ title: 'Projects', url: `/${orgSlug}/`, icon: <FolderIcon /> },
|
{ title: 'Projects', url: `/${orgSlug}/`, icon: <FolderIcon /> },
|
||||||
@ -118,9 +119,10 @@ const Sidebar = () => {
|
|||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow flex flex-col justify-end mb-8">
|
{/* Bottom navigation */}
|
||||||
|
<div className="flex flex-col justify-end">
|
||||||
<Tabs defaultValue="Projects" orientation="vertical">
|
<Tabs defaultValue="Projects" orientation="vertical">
|
||||||
{/* TODO: use proper link buttons */}
|
{/* // TODO: use proper link buttons */}
|
||||||
<Tabs.List>
|
<Tabs.List>
|
||||||
<Tabs.Trigger icon={<GlobeIcon />} value="">
|
<Tabs.Trigger icon={<GlobeIcon />} value="">
|
||||||
<a className="cursor-pointer" onClick={handleLogOut}>
|
<a className="cursor-pointer" onClick={handleLogOut}>
|
||||||
@ -139,5 +141,3 @@ const Sidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
|
1
packages/frontend/src/components/shared/Sidebar/index.ts
Normal file
1
packages/frontend/src/components/shared/Sidebar/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './Sidebar';
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import Sidebar from '../components/Sidebar';
|
import { OctokitProvider } from 'context/OctokitContext';
|
||||||
import { OctokitProvider } from '../context/OctokitContext';
|
import { Sidebar } from 'components/shared/Sidebar';
|
||||||
|
|
||||||
const OrgSlug = () => {
|
const OrgSlug = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -3,11 +3,11 @@ import { Link, useParams } from 'react-router-dom';
|
|||||||
import { Project } from 'gql-client';
|
import { Project } from 'gql-client';
|
||||||
import { Button } from 'components/shared/Button';
|
import { Button } from 'components/shared/Button';
|
||||||
|
|
||||||
import { Typography, Chip } from '@material-tailwind/react';
|
|
||||||
|
|
||||||
import { useGQLClient } from '../../context/GQLClientContext';
|
|
||||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||||
import { ProjectCard } from 'components/projects/ProjectCard';
|
import { ProjectCard } from 'components/projects/ProjectCard';
|
||||||
|
import { Heading } from 'components/shared/Heading';
|
||||||
|
import { Badge } from 'components/shared/Badge';
|
||||||
|
import { useGQLClient } from 'context/GQLClientContext';
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
@ -26,33 +26,31 @@ const Projects = () => {
|
|||||||
}, [orgSlug]);
|
}, [orgSlug]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<section className="px-6 py-6 flex flex-col gap-6">
|
||||||
<div className="flex p-5">
|
{/* Header */}
|
||||||
|
<div className="flex items-center">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<Typography variant="h4" placeholder={''}>
|
<Heading as="h2" className="text-[24px]">
|
||||||
Projects
|
Projects
|
||||||
</Typography>
|
</Heading>
|
||||||
<Chip
|
<Badge className="bg-base-bg-alternate text-elements-mid-em h-7 w-7">
|
||||||
className="bg-gray-300 rounded-full static"
|
{projects.length}
|
||||||
value={projects.length}
|
</Badge>
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Link to="projects/create">
|
<Link to="projects/create">
|
||||||
<Button leftIcon={<PlusIcon />}>Create project</Button>
|
<Button leftIcon={<PlusIcon />}>Create project</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/* List of projects */}
|
||||||
<div className="grid grid-cols-3 gap-5 p-5">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{projects.length !== 0 &&
|
{projects.length > 0 &&
|
||||||
projects.map((project, key) => {
|
projects.map((project, key) => {
|
||||||
return <ProjectCard project={project} key={key} />;
|
return <ProjectCard project={project} key={key} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
import {
|
||||||
|
type ElementType,
|
||||||
|
type ComponentPropsWithoutRef,
|
||||||
|
forwardRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a type by excluding common keys from one type to another.
|
* Construct a type by excluding common keys from one type to another.
|
||||||
* @template T - The type from which to omit properties.
|
* @template T - The type from which to omit properties.
|
||||||
@ -7,3 +13,15 @@
|
|||||||
* @returns A new type that includes all properties from T except those that are common with U.
|
* @returns A new type that includes all properties from T except those that are common with U.
|
||||||
*/
|
*/
|
||||||
export type OmitCommon<T, U> = Pick<T, Exclude<keyof T, keyof U>>;
|
export type OmitCommon<T, U> = Pick<T, Exclude<keyof T, keyof U>>;
|
||||||
|
|
||||||
|
export type PolymorphicProps<Element extends ElementType, Props> = Props &
|
||||||
|
Omit<ComponentPropsWithoutRef<Element>, 'as'> & {
|
||||||
|
as?: Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
// taken from : https://github.com/total-typescript/react-typescript-tutorial/blob/main/src/08-advanced-patterns/72-as-prop-with-forward-ref.solution.tsx
|
||||||
|
type FixedForwardRef = <T, P = object>(
|
||||||
|
render: (props: P, ref: React.Ref<T>) => React.ReactNode,
|
||||||
|
) => (props: P & React.RefAttributes<T>) => JSX.Element;
|
||||||
|
|
||||||
|
export const fixedForwardRef = forwardRef as FixedForwardRef;
|
||||||
|
Loading…
Reference in New Issue
Block a user