mirror of
https://gitea.gofwd.group/dstrawsb/ballistic-builder.git
synced 2025-12-06 02:36:44 -05:00
moved stuff out of app since it is used for routing
This commit is contained in:
20
src/components/About/index.tsx
Normal file
20
src/components/About/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function About() {
|
||||
|
||||
return (
|
||||
|
||||
(
|
||||
<section id="about" className="bg-gray-200 py-20">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<h3 className="text-3xl font-bold mb-6">About Us</h3>
|
||||
<p className="text-gray-700">
|
||||
Ballistic Builderis your go-to platform for customizing, building,
|
||||
and exploring firearm parts. Designed for enthusiasts by
|
||||
enthusiasts, we make firearm building easy and accessible.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
)
|
||||
}
|
||||
10
src/components/Base_Component/index.tsx
Normal file
10
src/components/Base_Component/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function BB_Base_Component() {
|
||||
|
||||
return (
|
||||
(
|
||||
<div />
|
||||
)
|
||||
)
|
||||
}
|
||||
24
src/components/Contact/index.tsx
Normal file
24
src/components/Contact/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Contact() {
|
||||
|
||||
return (
|
||||
(
|
||||
<section id="contact" className="py-20">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<h3 className="text-3xl font-bold mb-6">Contact Us</h3>
|
||||
<p className="text-gray-700 mb-6">
|
||||
Have questions or feedback? We’d love to hear from you!
|
||||
</p>
|
||||
<Link
|
||||
href="mailto:support@firearmbuilder.com"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
|
||||
Email Us
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
)
|
||||
}
|
||||
34
src/components/FeaturesSection/index.tsx
Normal file
34
src/components/FeaturesSection/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function FeaturesSection() {
|
||||
|
||||
return (
|
||||
(
|
||||
<section id="features" className="py-20">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<h3 className="text-3xl font-bold mb-6">Features</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="bg-white shadow-md p-6 rounded">
|
||||
<h4 className="text-xl font-bold mb-2">Extensive Database</h4>
|
||||
<p className="text-gray-600">
|
||||
Access thousands of firearm parts from trusted resellers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white shadow-md p-6 rounded">
|
||||
<h4 className="text-xl font-bold mb-2">Compatibility Checker</h4>
|
||||
<p className="text-gray-600">
|
||||
Ensure every part works perfectly together.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white shadow-md p-6 rounded">
|
||||
<h4 className="text-xl font-bold mb-2">Save & Share Builds</h4>
|
||||
<p className="text-gray-600">
|
||||
Save your builds or share them with friends.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
)
|
||||
}
|
||||
14
src/components/Footer /index.tsx
Normal file
14
src/components/Footer /index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
|
||||
return (
|
||||
(
|
||||
<footer className="bg-gray-800 text-white py-4">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<p>© {new Date().getFullYear()} Firearm Builder. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
)
|
||||
}
|
||||
74
src/components/GB_Footer/FooterLinks/index copy.js
Normal file
74
src/components/GB_Footer/FooterLinks/index copy.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import styles from './styles.module.css'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import TypoGraphy from '@mui/material/Typography'
|
||||
import Link from 'next/link'
|
||||
import { makeStyles } from '@mui/material/styles';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
width: '75%',
|
||||
maxWidth: 260,
|
||||
/* backgroundColor: theme.palette.background.paper, */
|
||||
float : 'right',
|
||||
marginRight:'2%',
|
||||
fontSize : '.80em'
|
||||
|
||||
|
||||
},
|
||||
}));
|
||||
|
||||
export default function FooterLinks() {
|
||||
|
||||
const classes = useStyles();
|
||||
return (
|
||||
(<div className={classes.root}>
|
||||
<TypoGraphy variant="subtitle1" color="inherit" >
|
||||
<List component="nav" >
|
||||
|
||||
<ListItemText inset >
|
||||
<TypoGraphy color="inherit" variant="subtitle2">
|
||||
<Link href="/info/faq" className={styles.navLinks}>FAQ</Link>
|
||||
</TypoGraphy>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset >
|
||||
<TypoGraphy color="inherit" variant="subtitle2">
|
||||
<Link href="/info/tos" className={styles.navLinks}>Terms Of Service</Link>
|
||||
</TypoGraphy>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset>
|
||||
<TypoGraphy color="inherit" variant="subtitle2">
|
||||
<Link href="/info/contactus" className={styles.navLinks}>Contact Us</Link>
|
||||
</TypoGraphy>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset>
|
||||
<TypoGraphy color="inherit" variant="subtitle2">
|
||||
<Link href="/info/privacypolicy" className={styles.navLinks}>Privacy Policy</Link>
|
||||
</TypoGraphy>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset>
|
||||
<TypoGraphy color="inherit" variant="subtitle2">
|
||||
<Link href="/info/pip" className={styles.navLinks}>Personal Information Policy</Link>
|
||||
</TypoGraphy>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset>
|
||||
<TypoGraphy color="inherit" variant="subtitle2">
|
||||
<Link href="/info/disclosure" className={styles.navLinks}>Disclosure</Link>
|
||||
</TypoGraphy>
|
||||
</ListItemText>
|
||||
|
||||
|
||||
</List>
|
||||
</TypoGraphy>
|
||||
</div>)
|
||||
);
|
||||
}
|
||||
|
||||
53
src/components/GB_Footer/FooterLinks/index.js
Normal file
53
src/components/GB_Footer/FooterLinks/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import styles from './styles.module.css'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import TypoGraphy from '@mui/material/Typography'
|
||||
import Link from 'next/link'
|
||||
import { withStyles } from '@mui/material/styles';
|
||||
import styled from '@emotion/styled'
|
||||
import FooterLink from '@/src/Fragments/FooterLink';
|
||||
|
||||
|
||||
class FooterLinks extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<FooterLinksStyled>
|
||||
<div className="footer-links">
|
||||
<List component="nav" >
|
||||
<React.Fragment>
|
||||
<FooterLink href="/info/faq" title="FAQ" />
|
||||
<FooterLink href="/info/tos" title="Terms Of Service" />
|
||||
<FooterLink href="/info/contactus" title="Contact Us" />
|
||||
<FooterLink href="/info/privacypolicy" title="Privacy Policy" />
|
||||
<FooterLink href="/info/pip" title="Personal Information Policy" />
|
||||
<FooterLink href="/info/disclosure" title="Disclosure" />
|
||||
<FooterLink href="/info/about" title="About Us" />
|
||||
</React.Fragment>
|
||||
</List>
|
||||
</div>
|
||||
</FooterLinksStyled>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const FooterLinksStyled = styled.div`
|
||||
.footer-links nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
// export default withStyles(useStyles)(FooterLinks);
|
||||
|
||||
export default FooterLinks;
|
||||
23
src/components/GB_Footer/FooterLinks/package.json
Normal file
23
src/components/GB_Footer/FooterLinks/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "footerlinks",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
src/components/GB_Footer/FooterLinks/styles.module.css
Normal file
7
src/components/GB_Footer/FooterLinks/styles.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.navLinks {
|
||||
color:#000;
|
||||
}
|
||||
.navLinks:hover {
|
||||
font-weight: bold;
|
||||
text-decoration : none;
|
||||
}
|
||||
43
src/components/GB_Footer/index.tsx
Normal file
43
src/components/GB_Footer/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import styles from './styles.module.scss';
|
||||
import Copyright from "@/src/components/GB_Info/Copyright";
|
||||
import FooterLinks from "./FooterLinks";
|
||||
import Link from "next/link";
|
||||
import { infoLinks } from "@/src/lib/linkList/infoLinks";
|
||||
import { sectionLinks } from "@/src/lib/linkList/sectionLinks";
|
||||
import Armory from '@/src/Fragments/Armory';
|
||||
import GroundZero from "@/src/Fragments/GroundZero";
|
||||
import Information from "@/src/Fragments/Information";
|
||||
export const Footer = () => {
|
||||
|
||||
return (
|
||||
// <div className={styles.Footer}>
|
||||
// <FooterLinks></FooterLinks>
|
||||
// <Copyright></Copyright>
|
||||
// </div>
|
||||
(<>
|
||||
<footer className={styles.footer}>
|
||||
<nav className={styles.linksContainer}>
|
||||
<div className={styles.brand}>
|
||||
<div className={styles.logo}>
|
||||
<span>Logo</span>
|
||||
</div>
|
||||
<p>Find Parts.</p>
|
||||
<p>Build Guns.</p>
|
||||
<p>Freedom On.</p>
|
||||
</div>
|
||||
<Armory titleText="Armory"/>
|
||||
<GroundZero titleText="Ground Zero"/>
|
||||
<Information titleText="Information"/>
|
||||
</nav>
|
||||
</footer>
|
||||
<Copyright></Copyright>
|
||||
</>)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Footer.propTypes = {};
|
||||
|
||||
export default Footer;
|
||||
23
src/components/GB_Footer/package.json
Normal file
23
src/components/GB_Footer/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "footer",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
59
src/components/GB_Footer/styles.module.scss
Normal file
59
src/components/GB_Footer/styles.module.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
@import '../../scss/variables.scss';
|
||||
|
||||
.footer {
|
||||
background: #4c5c3f;
|
||||
min-height: 300px;
|
||||
height: 100%;
|
||||
padding: 1rem 0;
|
||||
border-top: 2px solid #000;
|
||||
|
||||
.brand {
|
||||
font-weight: bolder;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 1.25em;
|
||||
color:#FFF;
|
||||
|
||||
.logo span {
|
||||
display: inline-block;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
background: #FFF;
|
||||
color:#4c5c3f;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linksContainer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
color:#FFF;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
transition: all 500ms ease;
|
||||
|
||||
&:hover {
|
||||
color: #ADA17B;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #FFF;
|
||||
border-bottom: 2px solid #fff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
106
src/components/GB_Header/index.tsx
Normal file
106
src/components/GB_Header/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types'; //ES6
|
||||
import styles from './styles.module.css';
|
||||
import Link from 'next/link';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material//ListItemText';
|
||||
// import TypoGraphy from '@mui/material/Typography';
|
||||
import Button from '@mui/material//Button';
|
||||
import { infoLinks } from '@/app/lib/linkList/infoLinks';
|
||||
import sectionLinks from '@/app/lib/linkList/sectionLinks';
|
||||
|
||||
export default class Header extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
(<div>
|
||||
<div className="topheader">
|
||||
<Link href="/" legacyBehavior><a className="logo">Gun Builder</a>
|
||||
</Link>
|
||||
</div>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<List component="nav">
|
||||
<ListItem component="div" className="nav-item">
|
||||
<ListItemText inset>
|
||||
<Link href={sectionLinks.UPPERS.URL} legacyBehavior><a className={styles.navLinks}>{sectionLinks.UPPERS.TEXT}</a></Link>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset>
|
||||
<Link href={sectionLinks.PARTSLIST.URL} legacyBehavior><a className={styles.navLinks}>{sectionLinks.PARTSLIST.TEXT}</a></Link>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset>
|
||||
<Link href={sectionLinks.BUILDS.URL} legacyBehavior><a className={styles.navLinks}>{sectionLinks.BUILDS.TEXT}</a></Link>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText inset>
|
||||
<Link href={sectionLinks.BLOG.URL} legacyBehavior><a className={styles.navLinks}>{sectionLinks.BLOG.TEXT}</a></Link>
|
||||
</ListItemText>
|
||||
|
||||
{/* <ListItemText inset>
|
||||
<Link href="/admin"><a className={styles.navLinks}>Admin</a></Link>
|
||||
</ListItemText> */}
|
||||
</ListItem>
|
||||
|
||||
</List>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<style jsx>{`
|
||||
header {
|
||||
background:#101010;
|
||||
color:#fff;
|
||||
}
|
||||
.topheader {
|
||||
background:#111;
|
||||
height: 4em;
|
||||
color: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topheader a {
|
||||
color:#fff;
|
||||
padding-left: 15px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
background: #4c5d34;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
ul li {
|
||||
margin-right: 10px;
|
||||
padding:1em 1.5em;
|
||||
border-right:2px solid rgba(0,0,0,.3);
|
||||
text-transform: uppercase;
|
||||
font-weight:bold;
|
||||
letter-spacing:2px;
|
||||
}
|
||||
`}</style>
|
||||
</div>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Header.propTypes = {
|
||||
|
||||
};
|
||||
23
src/components/GB_Header/package.json
Normal file
23
src/components/GB_Header/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "header",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
src/components/GB_Header/styles.module.css
Normal file
13
src/components/GB_Header/styles.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.navLinks {
|
||||
color:white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
font-weight: bold;
|
||||
transition: all 500ms ease;
|
||||
}
|
||||
.navLinks:hover {
|
||||
text-decoration : none;
|
||||
}
|
||||
.nav-item:hover {
|
||||
background-color: pink;
|
||||
}
|
||||
45
src/components/GB_Hero/index.tsx
Normal file
45
src/components/GB_Hero/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MuiLink from '@mui/material/Link';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
export default class Hero extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
||||
<div className="hero" styles={{ backgroundImage:`url({${this.props.image}})` }}>
|
||||
<div className="hero-text">
|
||||
<h3>{this.props.heading}</h3>
|
||||
<p>{this.props.subheading}</p>
|
||||
<Button href={this.props.link} variant="contained" color="primary">
|
||||
{this.props.linktext}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.hero {
|
||||
// background:url('/gb-hero.jpg');
|
||||
background-size:cover;
|
||||
min-height:35vw;
|
||||
color: #fff;
|
||||
display:flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.hero-text {
|
||||
padding: 2em;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
23
src/components/GB_Hero/package.json
Normal file
23
src/components/GB_Hero/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "hero",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
src/components/GB_Hero/styles.module.css
Normal file
0
src/components/GB_Hero/styles.module.css
Normal file
35
src/components/GB_Info/About/index.tsx
Normal file
35
src/components/GB_Info/About/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import styles from './styles.module.css'
|
||||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
|
||||
export default function About(props) {
|
||||
|
||||
const GET_SITE_CONTENT = gql`
|
||||
query Get_Site_Content {
|
||||
site_contents(where: {content_id: {_eq: "ABOUTUS"}}, order_by: {content: asc}) {
|
||||
id
|
||||
content_id
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(GET_SITE_CONTENT);
|
||||
if (loading) return "Loading ...";
|
||||
if (error) return `Error! ${error.message}`;
|
||||
|
||||
return (
|
||||
<div >
|
||||
<h1>About</h1>
|
||||
{data.site_contents.map((site_content: { content: any; }) => (
|
||||
<span dangerouslySetInnerHTML={{ __html: site_content.content }} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
About.propTypes = {
|
||||
|
||||
};
|
||||
23
src/components/GB_Info/About/package.json
Normal file
23
src/components/GB_Info/About/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "about",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
src/components/GB_Info/About/styles.module.css
Normal file
0
src/components/GB_Info/About/styles.module.css
Normal file
39
src/components/GB_Info/ContactUs/index.tsx
Normal file
39
src/components/GB_Info/ContactUs/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react'
|
||||
import TypoGraphy from '@mui/material/Typography';
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import styles from './styles.module.css';
|
||||
import constants from '@/src/lib/constants'
|
||||
|
||||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
|
||||
export default function ContactUs(props) {
|
||||
|
||||
const GET_SITE_CONTENT = gql`
|
||||
query Get_Site_Content {
|
||||
site_contents(where: {content_id: {_eq: "CONTACTUS"}}, order_by: {content: asc}) {
|
||||
id
|
||||
content_id
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(GET_SITE_CONTENT);
|
||||
if (loading) return "Loading ...";
|
||||
if (error) return `Error! ${error.message}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TypoGraphy paragraph='true' variant="body" color="inherit" >
|
||||
{data.site_contents.map((site_content: { content: any; }) => (
|
||||
<span dangerouslySetInnerHTML={{ __html: site_content.content }} />
|
||||
))}
|
||||
</TypoGraphy>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
ContactUs.propTypes = {
|
||||
|
||||
};
|
||||
23
src/components/GB_Info/ContactUs/package.json
Normal file
23
src/components/GB_Info/ContactUs/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "contactus",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
src/components/GB_Info/ContactUs/styles.module.css
Normal file
0
src/components/GB_Info/ContactUs/styles.module.css
Normal file
40
src/components/GB_Info/Copyright/index.tsx
Normal file
40
src/components/GB_Info/Copyright/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { Component } from 'react';
|
||||
import { COMPANY_NAME, COMPANY_URL } from '@/src/lib/constants';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import MuiLink from '@mui/material/Link';
|
||||
import styles from './styles.module.css'
|
||||
import Link from 'next/link'
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
|
||||
export default class Copyright extends Component {
|
||||
render() {
|
||||
return (
|
||||
(<CopyStyled>
|
||||
<div className="copyright">© {new Date().getFullYear()} {' '}
|
||||
<Link href={COMPANY_URL}>
|
||||
{COMPANY_NAME}
|
||||
</Link>{' '}
|
||||
<span>All Rights Reserved.</span>
|
||||
</div>
|
||||
</CopyStyled>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CopyStyled = styled.div`
|
||||
.copyright {
|
||||
background: #4c5c3f;
|
||||
font-size:.80em;
|
||||
text-transform: uppercase;
|
||||
color:#FFF;
|
||||
display: block;
|
||||
width:100%;
|
||||
text-align:center;
|
||||
|
||||
a {
|
||||
color:#FFF;
|
||||
text-decoration:none;
|
||||
}
|
||||
}
|
||||
`
|
||||
23
src/components/GB_Info/Copyright/package.json
Normal file
23
src/components/GB_Info/Copyright/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "copyright",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
src/components/GB_Info/Copyright/styles.module.css
Normal file
7
src/components/GB_Info/Copyright/styles.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.white {
|
||||
color: white;
|
||||
font-size: .5em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
font-family: "hind";
|
||||
}
|
||||
54
src/components/GB_Info/Disclosure/index.tsx
Normal file
54
src/components/GB_Info/Disclosure/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react'
|
||||
import TypoGraphy from '@mui/material/Typography';
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import styles from './styles.module.css';
|
||||
import constants from '@/src/lib/constants'
|
||||
import {SITE_CONT_TYPE} from '@/src/lib/constants'
|
||||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
|
||||
export default function Disclosure(props) {
|
||||
|
||||
const GET_SITE_CONTENT = gql`
|
||||
query Get_Site_Content {
|
||||
site_contents(where: {content_id: {_eq: ${SITE_CONT_TYPE.DISCLOSURE}}}, order_by: {content: asc}) {
|
||||
id
|
||||
content_id
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(GET_SITE_CONTENT);
|
||||
if (loading) return "Loading ...";
|
||||
if (error) return `Error! ${error.message}`;
|
||||
return (
|
||||
<div>
|
||||
<TypoGraphy paragraph={true} variant="body1" color="inherit" >
|
||||
{constants.SITE_NAME}, owned by {constants.COMPANY_NAME}, receives compensation through affiliate relationships with merchants listed on this site. Please know that this in no way affects reviews, benchmarks, content, or this site's opinions of products, services, manufacturers, partners, or merchants.
|
||||
</TypoGraphy>
|
||||
<TypoGraphy paragraph={true} variant="body1" color="inherit" >
|
||||
The mission of {constants.SITE_NAME} is to provide the best functionality for this site's users, regardless of any potential affiliate commissions.
|
||||
{constants.SITE_NAME} does not accept donations. Instead, income received from affiliate relationships funds site maintenance and feature development. If you desire to donate to PCPartPicker, I kindly ask that you consider donating to a charitable organization instead. I am particularly fond of the NRA, a non-profit organization that provides safety training to gun owners.
|
||||
</TypoGraphy>
|
||||
<div>
|
||||
{data.site_contents.map((site_content: { content: any; }) => (
|
||||
<span dangerouslySetInnerHTML={{ __html: site_content.content }} />
|
||||
))}
|
||||
</div>
|
||||
<TypoGraphy paragraph={true} variant="body1" color="inherit" >
|
||||
Thanks,
|
||||
</TypoGraphy>
|
||||
<TypoGraphy paragraph={true} variant="body1" color="inherit" >
|
||||
{constants.COMPANY_NAME}
|
||||
</TypoGraphy>
|
||||
<TypoGraphy paragraph={true} variant="body1" color="inherit" >
|
||||
{constants.SITE_NAME} is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com.
|
||||
</TypoGraphy>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Disclosure.propTypes = {
|
||||
|
||||
};
|
||||
23
src/components/GB_Info/Disclosure/package.json
Normal file
23
src/components/GB_Info/Disclosure/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "disclosure",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
src/components/GB_Info/Disclosure/styles.module.css
Normal file
0
src/components/GB_Info/Disclosure/styles.module.css
Normal file
34
src/components/GB_Info/Faq/index.tsx
Normal file
34
src/components/GB_Info/Faq/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import styles from './styles.module.css'
|
||||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
|
||||
export default function Faq(props) {
|
||||
|
||||
const GET_SITE_CONTENT_FAQ = gql`
|
||||
query Get_Site_Content_Faq {
|
||||
site_contents(where: {content_id: {_eq: "FAQ"}}, order_by: {content: asc}) {
|
||||
id
|
||||
content_id
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(GET_SITE_CONTENT_FAQ);
|
||||
if(loading) return "Loading ...";
|
||||
if(error) return `Error! ${error.message}` ;
|
||||
|
||||
return (
|
||||
|
||||
<div className="faq container">
|
||||
{props.children}
|
||||
<h3>Frequently Asked Questions</h3>
|
||||
<div>
|
||||
{data.site_contents.map((site_content: { content: any; }) => (
|
||||
<span dangerouslySetInnerHTML={{__html: site_content.content}}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
src/components/GB_Info/Faq/package.json
Normal file
23
src/components/GB_Info/Faq/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "faq",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
src/components/GB_Info/Faq/styles.module.css
Normal file
0
src/components/GB_Info/Faq/styles.module.css
Normal file
39
src/components/GB_Info/PIP/index.tsx
Normal file
39
src/components/GB_Info/PIP/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react'
|
||||
import Head from 'next/head';
|
||||
import TypoGraphy from '@mui/material/Typography';
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import styles from './styles.module.css';
|
||||
import constants from '@/src/lib/constants'
|
||||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
|
||||
export default function PIP(props) {
|
||||
const GET_SITE_CONTENT = gql`
|
||||
query Get_Site_Content {
|
||||
site_contents(where: {content_id: {_eq: "PIP"}}, order_by: {content: asc}) {
|
||||
id
|
||||
content_id
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(GET_SITE_CONTENT);
|
||||
if (loading) return "Loading ...";
|
||||
if (error) return `Error! ${error.message}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head title="Personal Information" />
|
||||
<TypoGraphy paragraph='true' variant="body" color="inherit" >
|
||||
|
||||
{data.site_contents.map((site_content: { content: any; }) => (
|
||||
<span dangerouslySetInnerHTML={{ __html: site_content.content }} />
|
||||
))}
|
||||
</TypoGraphy>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
PIP.propTypes = {
|
||||
|
||||
};
|
||||
23
src/components/GB_Info/PIP/package.json
Normal file
23
src/components/GB_Info/PIP/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "pip",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
src/components/GB_Info/PIP/styles.module.css
Normal file
0
src/components/GB_Info/PIP/styles.module.css
Normal file
35
src/components/GB_Info/PrivacyPolicy/index.tsx
Normal file
35
src/components/GB_Info/PrivacyPolicy/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { Component } from 'react'
|
||||
import TypoGraphy from '@mui/material/Typography';
|
||||
import Head from 'next/head';
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import styles from './styles.module.css';
|
||||
import constants from '@/src/lib/constants'
|
||||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
|
||||
export default function PrivacyPolicy(props) {
|
||||
const GET_SITE_CONTENT = gql`
|
||||
query Get_Site_Content {
|
||||
site_contents(where: {content_id: {_eq: "PP"}}, order_by: {content: asc}) {
|
||||
id
|
||||
content_id
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(GET_SITE_CONTENT);
|
||||
if (loading) return "Loading ...";
|
||||
if (error) return `Error! ${error.message}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head title="Privacy Policy" />
|
||||
<TypoGraphy paragraph='true' variant="body" color="inherit" >
|
||||
{data.site_contents.map((site_content: { content: any; }) => (
|
||||
<span dangerouslySetInnerHTML={{ __html: site_content.content }} />
|
||||
))}
|
||||
</TypoGraphy>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
23
src/components/GB_Info/PrivacyPolicy/package.json
Normal file
23
src/components/GB_Info/PrivacyPolicy/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "privacypolicy",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
src/components/GB_Info/TermsOfService/index.tsx
Normal file
38
src/components/GB_Info/TermsOfService/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react'
|
||||
import Head from 'next/head';
|
||||
import TypoGraphy from '@mui/material/Typography';
|
||||
import PropTypes from 'prop-types' //ES6
|
||||
import constants from '@/src/lib/constants'
|
||||
import { useQuery, useMutation, gql } from "@apollo/client";
|
||||
|
||||
export default function TermsOfService(props) {
|
||||
const GET_SITE_CONTENT = gql`
|
||||
query Get_Site_Content {
|
||||
site_contents(where: {content_id: {_eq: "TOS"}}, order_by: {content: asc}) {
|
||||
id
|
||||
content_id
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { loading, error, data } = useQuery(GET_SITE_CONTENT);
|
||||
if (loading) return "Loading ...";
|
||||
if (error) return `Error! ${error.message}`;
|
||||
return (
|
||||
<div>
|
||||
<Head title="Terms Of Service" />
|
||||
<TypoGraphy paragraph='true' variant="body" color="inherit" >
|
||||
<div>
|
||||
{data.site_contents.map((site_content: { content: any; }) => (
|
||||
<span dangerouslySetInnerHTML={{ __html: site_content.content }} />
|
||||
))}
|
||||
</div>
|
||||
</TypoGraphy>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TermsOfService.propTypes = {
|
||||
|
||||
};
|
||||
23
src/components/GB_Info/TermsOfService/package.json
Normal file
23
src/components/GB_Info/TermsOfService/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "termsofservice",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index.tsx",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
132
src/components/GB_SignIn/index.tsx
Normal file
132
src/components/GB_SignIn/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import Link from '@mui/material/Link';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Box from '@mui/material/Box';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { makeStyles } from '@mui/material/styles';
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
<Typography variant="body2" color="textSecondary" align="center">
|
||||
{'Copyright © '}
|
||||
<Link color="inherit" href="https://material-ui.com/">
|
||||
Your Website
|
||||
</Link>{' '}
|
||||
{new Date().getFullYear()}
|
||||
{'.'}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
height: '100vh',
|
||||
},
|
||||
image: {
|
||||
backgroundImage: 'url(https://source.unsplash.com/random)',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundColor:
|
||||
theme.palette.type === 'light' ? theme.palette.grey[50] : theme.palette.grey[900],
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
},
|
||||
paper: {
|
||||
margin: theme.spacing(8, 4),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatar: {
|
||||
margin: theme.spacing(1),
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
form: {
|
||||
width: '100%', // Fix IE 11 issue.
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
submit: {
|
||||
margin: theme.spacing(3, 0, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SignInSide() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Grid container component="main" className={classes.root}>
|
||||
<CssBaseline />
|
||||
<Grid item xs={false} sm={4} md={7} className={classes.image} />
|
||||
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
|
||||
<div className={classes.paper}>
|
||||
<Avatar className={classes.avatar}>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
Sign in
|
||||
</Typography>
|
||||
<form className={classes.form} noValidate>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="email"
|
||||
label="Email Address"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox value="remember" color="primary" />}
|
||||
label="Remember me"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.submit}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<Grid container>
|
||||
<Grid item xs>
|
||||
<Link href="#" variant="body2">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Link href="#" variant="body2">
|
||||
{"Don't have an account? Sign Up"}
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box mt={5}>
|
||||
<Copyright />
|
||||
</Box>
|
||||
</form>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
23
src/components/GB_SignIn/package.json
Normal file
23
src/components/GB_SignIn/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "signin",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "./index",
|
||||
"author": {
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Don Strawsburg",
|
||||
"email": "don@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
},
|
||||
{
|
||||
"name": "Sean Strawsburg",
|
||||
"email": "sean@goforward.group",
|
||||
"url": "https://goforward.group/"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
src/components/GB_SignIn/styles.module.css
Normal file
1
src/components/GB_SignIn/styles.module.css
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
39
src/components/Header/Header.tsx
Normal file
39
src/components/Header/Header.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Box, Flex, Link, Heading } from "@chakra-ui/react";
|
||||
import NextLink from "next/link";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Box as="header" bg="primary" color="white" px="6" py="4" shadow="md">
|
||||
<Flex justify="space-between" align="center" maxW="5xl" mx="auto">
|
||||
<Heading as="h1" size="lg">
|
||||
<NextLink href="/" passHref>
|
||||
<Link color="white" _hover={{ textDecoration: "none" }}>
|
||||
Ballistic Builder
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Heading>
|
||||
<Flex as="nav" gap="6">
|
||||
<NextLink href="/builder" passHref>
|
||||
<Link color="white" _hover={{ textDecoration: "underline" }}>
|
||||
Builder
|
||||
</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/products" passHref>
|
||||
<Link color="white" _hover={{ textDecoration: "underline" }}>
|
||||
Products
|
||||
</Link>
|
||||
</NextLink>
|
||||
<NextLink href="/auth/signin" passHref>
|
||||
<Link color="white" _hover={{ textDecoration: "underline" }}>
|
||||
Sign In
|
||||
</Link>
|
||||
</NextLink>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
23
src/components/Header/index.tsx
Normal file
23
src/components/Header/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Header() {
|
||||
{/* Header Section */ }
|
||||
return (
|
||||
(
|
||||
<header className="bg-gray-800 text-white py-4 shadow-md">
|
||||
<div className="container mx-auto px-6 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">Ballistic Builder</h1>
|
||||
<nav>
|
||||
<ul className="flex space-x-4">
|
||||
<li><Link href="#features" className="hover:underline">Features</Link></li>
|
||||
<li><Link href="/builder" className="hover:underline">Builder</Link></li>
|
||||
<li><Link href="/products" className="hover:underline">Products</Link></li>
|
||||
<li><Link href="#contact" className="hover:underline">Contact</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
|
||||
)
|
||||
)}
|
||||
24
src/components/Hero/index.tsx
Normal file
24
src/components/Hero/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Hero() {
|
||||
{/* Hero Section */ }
|
||||
return (
|
||||
|
||||
<section className="bg-gray-700 text-white py-20 text-center">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-4xl font-bold mb-4">Build Your Dream Firearm</h2>
|
||||
<p className="text-lg mb-6">
|
||||
Customize every component of your firearm with ease and precision.
|
||||
</p>
|
||||
<Link
|
||||
href="/builder"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
|
||||
Get Started
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
)
|
||||
}
|
||||
24
src/components/HomeContent/index.tsx
Normal file
24
src/components/HomeContent/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Link from "next/link";
|
||||
import Header from "../Header";
|
||||
import Hero from "../Hero";
|
||||
import FeaturesSection from "../FeaturesSection";
|
||||
import About from "../About";
|
||||
import Contact from "../Contact";
|
||||
import Footer from "../Footer ";
|
||||
|
||||
export default function HomeContent() {
|
||||
|
||||
return (
|
||||
(
|
||||
<>
|
||||
<Header />
|
||||
<Hero />
|
||||
<FeaturesSection />
|
||||
<About />
|
||||
<Contact />
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
47
src/components/ui/accordion.tsx
Normal file
47
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Accordion, HStack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuChevronDown } from "react-icons/lu"
|
||||
|
||||
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
|
||||
indicatorPlacement?: "start" | "end"
|
||||
}
|
||||
|
||||
export const AccordionItemTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
AccordionItemTriggerProps
|
||||
>(function AccordionItemTrigger(props, ref) {
|
||||
const { children, indicatorPlacement = "end", ...rest } = props
|
||||
return (
|
||||
<Accordion.ItemTrigger {...rest} ref={ref}>
|
||||
{indicatorPlacement === "start" && (
|
||||
<Accordion.ItemIndicator rotate={{ base: "-90deg", _open: "0deg" }}>
|
||||
<LuChevronDown />
|
||||
</Accordion.ItemIndicator>
|
||||
)}
|
||||
<HStack gap="4" flex="1" textAlign="start" width="full">
|
||||
{children}
|
||||
</HStack>
|
||||
{indicatorPlacement === "end" && (
|
||||
<Accordion.ItemIndicator>
|
||||
<LuChevronDown />
|
||||
</Accordion.ItemIndicator>
|
||||
)}
|
||||
</Accordion.ItemTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
interface AccordionItemContentProps extends Accordion.ItemContentProps {}
|
||||
|
||||
export const AccordionItemContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionItemContentProps
|
||||
>(function AccordionItemContent(props, ref) {
|
||||
return (
|
||||
<Accordion.ItemContent>
|
||||
<Accordion.ItemBody {...props} ref={ref} />
|
||||
</Accordion.ItemContent>
|
||||
)
|
||||
})
|
||||
|
||||
export const AccordionRoot = Accordion.Root
|
||||
export const AccordionItem = Accordion.Item
|
||||
40
src/components/ui/action-bar.tsx
Normal file
40
src/components/ui/action-bar.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ActionBar, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface ActionBarContentProps extends ActionBar.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const ActionBarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ActionBarContentProps
|
||||
>(function ActionBarContent(props, ref) {
|
||||
const { children, portalled = true, portalRef, ...rest } = props
|
||||
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ActionBar.Positioner>
|
||||
<ActionBar.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ActionBar.Content>
|
||||
</ActionBar.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const ActionBarCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ActionBar.CloseTriggerProps
|
||||
>(function ActionBarCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ActionBar.CloseTrigger {...props} asChild ref={ref}>
|
||||
<CloseButton size="sm" />
|
||||
</ActionBar.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const ActionBarRoot = ActionBar.Root
|
||||
export const ActionBarSelectionTrigger = ActionBar.SelectionTrigger
|
||||
export const ActionBarSeparator = ActionBar.Separator
|
||||
51
src/components/ui/alert.tsx
Normal file
51
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Alert as ChakraAlert } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
export interface AlertProps extends Omit<ChakraAlert.RootProps, "title"> {
|
||||
startElement?: React.ReactNode
|
||||
endElement?: React.ReactNode
|
||||
title?: React.ReactNode
|
||||
icon?: React.ReactElement
|
||||
closable?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||
function Alert(props, ref) {
|
||||
const {
|
||||
title,
|
||||
children,
|
||||
icon,
|
||||
closable,
|
||||
onClose,
|
||||
startElement,
|
||||
endElement,
|
||||
...rest
|
||||
} = props
|
||||
return (
|
||||
<ChakraAlert.Root ref={ref} {...rest}>
|
||||
{startElement || <ChakraAlert.Indicator>{icon}</ChakraAlert.Indicator>}
|
||||
{children ? (
|
||||
<ChakraAlert.Content>
|
||||
<ChakraAlert.Title>{title}</ChakraAlert.Title>
|
||||
<ChakraAlert.Description>{children}</ChakraAlert.Description>
|
||||
</ChakraAlert.Content>
|
||||
) : (
|
||||
<ChakraAlert.Title flex="1">{title}</ChakraAlert.Title>
|
||||
)}
|
||||
{endElement}
|
||||
{closable && (
|
||||
<CloseButton
|
||||
size="sm"
|
||||
pos="relative"
|
||||
top="-2"
|
||||
insetEnd="-2"
|
||||
alignSelf="flex-start"
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</ChakraAlert.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
74
src/components/ui/avatar.tsx
Normal file
74
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
|
||||
import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react"
|
||||
import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
|
||||
|
||||
export interface AvatarProps extends ChakraAvatar.RootProps {
|
||||
name?: string
|
||||
src?: string
|
||||
srcSet?: string
|
||||
loading?: ImageProps["loading"]
|
||||
icon?: React.ReactElement
|
||||
fallback?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||
function Avatar(props, ref) {
|
||||
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
|
||||
props
|
||||
return (
|
||||
<ChakraAvatar.Root ref={ref} {...rest}>
|
||||
<AvatarFallback name={name} icon={icon}>
|
||||
{fallback}
|
||||
</AvatarFallback>
|
||||
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
||||
{children}
|
||||
</ChakraAvatar.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface AvatarFallbackProps extends ChakraAvatar.FallbackProps {
|
||||
name?: string
|
||||
icon?: React.ReactElement
|
||||
}
|
||||
|
||||
const AvatarFallback = React.forwardRef<HTMLDivElement, AvatarFallbackProps>(
|
||||
function AvatarFallback(props, ref) {
|
||||
const { name, icon, children, ...rest } = props
|
||||
return (
|
||||
<ChakraAvatar.Fallback ref={ref} {...rest}>
|
||||
{children}
|
||||
{name != null && children == null && <>{getInitials(name)}</>}
|
||||
{name == null && children == null && (
|
||||
<ChakraAvatar.Icon asChild={!!icon}>{icon}</ChakraAvatar.Icon>
|
||||
)}
|
||||
</ChakraAvatar.Fallback>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function getInitials(name: string) {
|
||||
const names = name.trim().split(" ")
|
||||
const firstName = names[0] != null ? names[0] : ""
|
||||
const lastName = names.length > 1 ? names[names.length - 1] : ""
|
||||
return firstName && lastName
|
||||
? `${firstName.charAt(0)}${lastName.charAt(0)}`
|
||||
: firstName.charAt(0)
|
||||
}
|
||||
|
||||
interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {}
|
||||
|
||||
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
||||
function AvatarGroup(props, ref) {
|
||||
const { size, variant, borderless, ...rest } = props
|
||||
return (
|
||||
<ChakraAvatar.PropsProvider value={{ size, variant, borderless }}>
|
||||
<Group gap="0" spaceX="-3" ref={ref} {...rest} />
|
||||
</ChakraAvatar.PropsProvider>
|
||||
)
|
||||
},
|
||||
)
|
||||
31
src/components/ui/blockquote.tsx
Normal file
31
src/components/ui/blockquote.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Blockquote as ChakraBlockquote } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface BlockquoteProps extends ChakraBlockquote.RootProps {
|
||||
cite?: React.ReactNode
|
||||
citeUrl?: string
|
||||
icon?: React.ReactNode
|
||||
showDash?: boolean
|
||||
}
|
||||
|
||||
export const Blockquote = React.forwardRef<HTMLDivElement, BlockquoteProps>(
|
||||
function Blockquote(props, ref) {
|
||||
const { children, cite, citeUrl, showDash, icon, ...rest } = props
|
||||
|
||||
return (
|
||||
<ChakraBlockquote.Root ref={ref} {...rest}>
|
||||
{icon}
|
||||
<ChakraBlockquote.Content cite={citeUrl}>
|
||||
{children}
|
||||
</ChakraBlockquote.Content>
|
||||
{cite && (
|
||||
<ChakraBlockquote.Caption>
|
||||
{showDash ? <>—</> : null} <cite>{cite}</cite>
|
||||
</ChakraBlockquote.Caption>
|
||||
)}
|
||||
</ChakraBlockquote.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const BlockquoteIcon = ChakraBlockquote.Icon
|
||||
40
src/components/ui/breadcrumb.tsx
Normal file
40
src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Breadcrumb, type SystemStyleObject } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface BreadcrumbRootProps extends Breadcrumb.RootProps {
|
||||
separator?: React.ReactNode
|
||||
separatorGap?: SystemStyleObject["gap"]
|
||||
}
|
||||
|
||||
export const BreadcrumbRoot = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
BreadcrumbRootProps
|
||||
>(function BreadcrumbRoot(props, ref) {
|
||||
const { separator, separatorGap, children, ...rest } = props
|
||||
|
||||
const validChildren = React.Children.toArray(children).filter(
|
||||
React.isValidElement,
|
||||
)
|
||||
|
||||
return (
|
||||
<Breadcrumb.Root ref={ref} {...rest}>
|
||||
<Breadcrumb.List gap={separatorGap}>
|
||||
{validChildren.map((child, index) => {
|
||||
const last = index === validChildren.length - 1
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Breadcrumb.Item>{child}</Breadcrumb.Item>
|
||||
{!last && (
|
||||
<Breadcrumb.Separator>{separator}</Breadcrumb.Separator>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
)
|
||||
})
|
||||
|
||||
export const BreadcrumbLink = Breadcrumb.Link
|
||||
export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink
|
||||
export const BreadcrumbEllipsis = Breadcrumb.Ellipsis
|
||||
40
src/components/ui/button.tsx
Normal file
40
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react"
|
||||
import {
|
||||
AbsoluteCenter,
|
||||
Button as ChakraButton,
|
||||
Span,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface ButtonLoadingProps {
|
||||
loading?: boolean
|
||||
loadingText?: React.ReactNode
|
||||
}
|
||||
|
||||
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function Button(props, ref) {
|
||||
const { loading, disabled, loadingText, children, ...rest } = props
|
||||
return (
|
||||
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
|
||||
{loading && !loadingText ? (
|
||||
<>
|
||||
<AbsoluteCenter display="inline-flex">
|
||||
<Spinner size="inherit" color="inherit" />
|
||||
</AbsoluteCenter>
|
||||
<Span opacity={0}>{children}</Span>
|
||||
</>
|
||||
) : loading && loadingText ? (
|
||||
<>
|
||||
<Spinner size="inherit" color="inherit" />
|
||||
{loadingText}
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ChakraButton>
|
||||
)
|
||||
},
|
||||
)
|
||||
58
src/components/ui/checkbox-card.tsx
Normal file
58
src/components/ui/checkbox-card.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { CheckboxCard as ChakraCheckboxCard } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps {
|
||||
icon?: React.ReactElement
|
||||
label?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
addon?: React.ReactNode
|
||||
indicator?: React.ReactNode | null
|
||||
indicatorPlacement?: "start" | "end" | "inside"
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const CheckboxCard = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
CheckboxCardProps
|
||||
>(function CheckboxCard(props, ref) {
|
||||
const {
|
||||
inputProps,
|
||||
label,
|
||||
description,
|
||||
icon,
|
||||
addon,
|
||||
indicator = <ChakraCheckboxCard.Indicator />,
|
||||
indicatorPlacement = "end",
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const hasContent = label || description || icon
|
||||
const ContentWrapper = indicator ? ChakraCheckboxCard.Content : React.Fragment
|
||||
|
||||
return (
|
||||
<ChakraCheckboxCard.Root {...rest}>
|
||||
<ChakraCheckboxCard.HiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraCheckboxCard.Control>
|
||||
{indicatorPlacement === "start" && indicator}
|
||||
{hasContent && (
|
||||
<ContentWrapper>
|
||||
{icon}
|
||||
{label && (
|
||||
<ChakraCheckboxCard.Label>{label}</ChakraCheckboxCard.Label>
|
||||
)}
|
||||
{description && (
|
||||
<ChakraCheckboxCard.Description>
|
||||
{description}
|
||||
</ChakraCheckboxCard.Description>
|
||||
)}
|
||||
{indicatorPlacement === "inside" && indicator}
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{indicatorPlacement === "end" && indicator}
|
||||
</ChakraCheckboxCard.Control>
|
||||
{addon && <ChakraCheckboxCard.Addon>{addon}</ChakraCheckboxCard.Addon>}
|
||||
</ChakraCheckboxCard.Root>
|
||||
)
|
||||
})
|
||||
|
||||
export const CheckboxCardIndicator = ChakraCheckboxCard.Indicator
|
||||
25
src/components/ui/checkbox.tsx
Normal file
25
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
||||
icon?: React.ReactNode
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
rootRef?: React.Ref<HTMLLabelElement>
|
||||
}
|
||||
|
||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
function Checkbox(props, ref) {
|
||||
const { icon, children, inputProps, rootRef, ...rest } = props
|
||||
return (
|
||||
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
||||
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraCheckbox.Control>
|
||||
{icon || <ChakraCheckbox.Indicator />}
|
||||
</ChakraCheckbox.Control>
|
||||
{children != null && (
|
||||
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
|
||||
)}
|
||||
</ChakraCheckbox.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
108
src/components/ui/clipboard.tsx
Normal file
108
src/components/ui/clipboard.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { ButtonProps, InputProps } from "@chakra-ui/react"
|
||||
import {
|
||||
Button,
|
||||
Clipboard as ChakraClipboard,
|
||||
IconButton,
|
||||
Input,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuCheck, LuClipboard, LuLink } from "react-icons/lu"
|
||||
|
||||
const ClipboardIcon = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraClipboard.IndicatorProps
|
||||
>(function ClipboardIcon(props, ref) {
|
||||
return (
|
||||
<ChakraClipboard.Indicator copied={<LuCheck />} {...props} ref={ref}>
|
||||
<LuClipboard />
|
||||
</ChakraClipboard.Indicator>
|
||||
)
|
||||
})
|
||||
|
||||
const ClipboardCopyText = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraClipboard.IndicatorProps
|
||||
>(function ClipboardCopyText(props, ref) {
|
||||
return (
|
||||
<ChakraClipboard.Indicator copied="Copied" {...props} ref={ref}>
|
||||
Copy
|
||||
</ChakraClipboard.Indicator>
|
||||
)
|
||||
})
|
||||
|
||||
export const ClipboardLabel = React.forwardRef<
|
||||
HTMLLabelElement,
|
||||
ChakraClipboard.LabelProps
|
||||
>(function ClipboardLabel(props, ref) {
|
||||
return (
|
||||
<ChakraClipboard.Label
|
||||
textStyle="sm"
|
||||
fontWeight="medium"
|
||||
display="inline-block"
|
||||
mb="1"
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export const ClipboardButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function ClipboardButton(props, ref) {
|
||||
return (
|
||||
<ChakraClipboard.Trigger asChild>
|
||||
<Button ref={ref} size="sm" variant="surface" {...props}>
|
||||
<ClipboardIcon />
|
||||
<ClipboardCopyText />
|
||||
</Button>
|
||||
</ChakraClipboard.Trigger>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const ClipboardLink = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function ClipboardLink(props, ref) {
|
||||
return (
|
||||
<ChakraClipboard.Trigger asChild>
|
||||
<Button
|
||||
unstyled
|
||||
variant="plain"
|
||||
size="xs"
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
gap="2"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<LuLink />
|
||||
<ClipboardCopyText />
|
||||
</Button>
|
||||
</ChakraClipboard.Trigger>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const ClipboardIconButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonProps
|
||||
>(function ClipboardIconButton(props, ref) {
|
||||
return (
|
||||
<ChakraClipboard.Trigger asChild>
|
||||
<IconButton ref={ref} size="xs" variant="subtle" {...props}>
|
||||
<ClipboardIcon />
|
||||
<ClipboardCopyText srOnly />
|
||||
</IconButton>
|
||||
</ChakraClipboard.Trigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const ClipboardInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
function ClipboardInputElement(props, ref) {
|
||||
return (
|
||||
<ChakraClipboard.Input asChild>
|
||||
<Input ref={ref} {...props} />
|
||||
</ChakraClipboard.Input>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const ClipboardRoot = ChakraClipboard.Root
|
||||
17
src/components/ui/close-button.tsx
Normal file
17
src/components/ui/close-button.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ButtonProps as ChakraCloseButtonProps } from "@chakra-ui/react"
|
||||
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuX } from "react-icons/lu"
|
||||
|
||||
export interface CloseButtonProps extends ChakraCloseButtonProps {}
|
||||
|
||||
export const CloseButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
CloseButtonProps
|
||||
>(function CloseButton(props, ref) {
|
||||
return (
|
||||
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
|
||||
{props.children ?? <LuX />}
|
||||
</ChakraIconButton>
|
||||
)
|
||||
})
|
||||
67
src/components/ui/color-mode.tsx
Normal file
67
src/components/ui/color-mode.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client"
|
||||
|
||||
import type { IconButtonProps } from "@chakra-ui/react"
|
||||
import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react"
|
||||
import { ThemeProvider, useTheme } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
import * as React from "react"
|
||||
import { LuMoon, LuSun } from "react-icons/lu"
|
||||
|
||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||
|
||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export function useColorMode() {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const toggleColorMode = () => {
|
||||
setTheme(resolvedTheme === "light" ? "dark" : "light")
|
||||
}
|
||||
return {
|
||||
colorMode: resolvedTheme,
|
||||
setColorMode: setTheme,
|
||||
toggleColorMode,
|
||||
}
|
||||
}
|
||||
|
||||
export function useColorModeValue<T>(light: T, dark: T) {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "light" ? light : dark
|
||||
}
|
||||
|
||||
export function ColorModeIcon() {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "light" ? <LuSun /> : <LuMoon />
|
||||
}
|
||||
|
||||
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||
|
||||
export const ColorModeButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ColorModeButtonProps
|
||||
>(function ColorModeButton(props, ref) {
|
||||
const { toggleColorMode } = useColorMode()
|
||||
return (
|
||||
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
aria-label="Toggle color mode"
|
||||
size="sm"
|
||||
ref={ref}
|
||||
{...props}
|
||||
css={{
|
||||
_icon: {
|
||||
width: "5",
|
||||
height: "5",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ColorModeIcon />
|
||||
</IconButton>
|
||||
</ClientOnly>
|
||||
)
|
||||
})
|
||||
30
src/components/ui/data-list.tsx
Normal file
30
src/components/ui/data-list.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { DataList as ChakraDataList } from "@chakra-ui/react"
|
||||
import { InfoTip } from "./toggle-tip"
|
||||
import * as React from "react"
|
||||
|
||||
export const DataListRoot = ChakraDataList.Root
|
||||
|
||||
interface ItemProps extends ChakraDataList.ItemProps {
|
||||
label: React.ReactNode
|
||||
value: React.ReactNode
|
||||
info?: React.ReactNode
|
||||
grow?: boolean
|
||||
}
|
||||
|
||||
export const DataListItem = React.forwardRef<HTMLDivElement, ItemProps>(
|
||||
function DataListItem(props, ref) {
|
||||
const { label, info, value, children, grow, ...rest } = props
|
||||
return (
|
||||
<ChakraDataList.Item ref={ref} {...rest}>
|
||||
<ChakraDataList.ItemLabel flex={grow ? "1" : undefined}>
|
||||
{label}
|
||||
{info && <InfoTip>{info}</InfoTip>}
|
||||
</ChakraDataList.ItemLabel>
|
||||
<ChakraDataList.ItemValue flex={grow ? "1" : undefined}>
|
||||
{value}
|
||||
</ChakraDataList.ItemValue>
|
||||
{children}
|
||||
</ChakraDataList.Item>
|
||||
)
|
||||
},
|
||||
)
|
||||
62
src/components/ui/dialog.tsx
Normal file
62
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface DialogContentProps extends ChakraDialog.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
backdrop?: boolean
|
||||
}
|
||||
|
||||
export const DialogContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DialogContentProps
|
||||
>(function DialogContent(props, ref) {
|
||||
const {
|
||||
children,
|
||||
portalled = true,
|
||||
portalRef,
|
||||
backdrop = true,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
{backdrop && <ChakraDialog.Backdrop />}
|
||||
<ChakraDialog.Positioner>
|
||||
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ChakraDialog.Content>
|
||||
</ChakraDialog.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const DialogCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraDialog.CloseTriggerProps
|
||||
>(function DialogCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraDialog.CloseTrigger
|
||||
position="absolute"
|
||||
top="2"
|
||||
insetEnd="2"
|
||||
{...props}
|
||||
asChild
|
||||
>
|
||||
<CloseButton size="sm" ref={ref}>
|
||||
{props.children}
|
||||
</CloseButton>
|
||||
</ChakraDialog.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const DialogRoot = ChakraDialog.Root
|
||||
export const DialogFooter = ChakraDialog.Footer
|
||||
export const DialogHeader = ChakraDialog.Header
|
||||
export const DialogBody = ChakraDialog.Body
|
||||
export const DialogBackdrop = ChakraDialog.Backdrop
|
||||
export const DialogTitle = ChakraDialog.Title
|
||||
export const DialogDescription = ChakraDialog.Description
|
||||
export const DialogTrigger = ChakraDialog.Trigger
|
||||
export const DialogActionTrigger = ChakraDialog.ActionTrigger
|
||||
52
src/components/ui/drawer.tsx
Normal file
52
src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface DrawerContentProps extends ChakraDrawer.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
offset?: ChakraDrawer.ContentProps["padding"]
|
||||
}
|
||||
|
||||
export const DrawerContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DrawerContentProps
|
||||
>(function DrawerContent(props, ref) {
|
||||
const { children, portalled = true, portalRef, offset, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraDrawer.Positioner padding={offset}>
|
||||
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
|
||||
{children}
|
||||
</ChakraDrawer.Content>
|
||||
</ChakraDrawer.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const DrawerCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraDrawer.CloseTriggerProps
|
||||
>(function DrawerCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraDrawer.CloseTrigger
|
||||
position="absolute"
|
||||
top="2"
|
||||
insetEnd="2"
|
||||
{...props}
|
||||
asChild
|
||||
>
|
||||
<CloseButton size="sm" ref={ref} />
|
||||
</ChakraDrawer.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const DrawerTrigger = ChakraDrawer.Trigger
|
||||
export const DrawerRoot = ChakraDrawer.Root
|
||||
export const DrawerFooter = ChakraDrawer.Footer
|
||||
export const DrawerHeader = ChakraDrawer.Header
|
||||
export const DrawerBody = ChakraDrawer.Body
|
||||
export const DrawerBackdrop = ChakraDrawer.Backdrop
|
||||
export const DrawerDescription = ChakraDrawer.Description
|
||||
export const DrawerTitle = ChakraDrawer.Title
|
||||
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
|
||||
34
src/components/ui/empty-state.tsx
Normal file
34
src/components/ui/empty-state.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { EmptyState as ChakraEmptyState, VStack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface EmptyStateProps extends ChakraEmptyState.RootProps {
|
||||
title: string
|
||||
description?: string
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
|
||||
function EmptyState(props, ref) {
|
||||
const { title, description, icon, children, ...rest } = props
|
||||
return (
|
||||
<ChakraEmptyState.Root ref={ref} {...rest}>
|
||||
<ChakraEmptyState.Content>
|
||||
{icon && (
|
||||
<ChakraEmptyState.Indicator>{icon}</ChakraEmptyState.Indicator>
|
||||
)}
|
||||
{description ? (
|
||||
<VStack textAlign="center">
|
||||
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
||||
<ChakraEmptyState.Description>
|
||||
{description}
|
||||
</ChakraEmptyState.Description>
|
||||
</VStack>
|
||||
) : (
|
||||
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
|
||||
)}
|
||||
{children}
|
||||
</ChakraEmptyState.Content>
|
||||
</ChakraEmptyState.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
33
src/components/ui/field.tsx
Normal file
33
src/components/ui/field.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Field as ChakraField } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
errorText?: React.ReactNode
|
||||
optionalText?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||
function Field(props, ref) {
|
||||
const { label, children, helperText, errorText, optionalText, ...rest } =
|
||||
props
|
||||
return (
|
||||
<ChakraField.Root ref={ref} {...rest}>
|
||||
{label && (
|
||||
<ChakraField.Label>
|
||||
{label}
|
||||
<ChakraField.RequiredIndicator fallback={optionalText} />
|
||||
</ChakraField.Label>
|
||||
)}
|
||||
{children}
|
||||
{helperText && (
|
||||
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
|
||||
)}
|
||||
{errorText && (
|
||||
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
|
||||
)}
|
||||
</ChakraField.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
170
src/components/ui/file-button.tsx
Normal file
170
src/components/ui/file-button.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client"
|
||||
|
||||
import type { ButtonProps, RecipeProps } from "@chakra-ui/react"
|
||||
import {
|
||||
Button,
|
||||
FileUpload as ChakraFileUpload,
|
||||
Icon,
|
||||
IconButton,
|
||||
Span,
|
||||
Text,
|
||||
useFileUploadContext,
|
||||
useRecipe,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuFile, LuUpload, LuX } from "react-icons/lu"
|
||||
|
||||
export interface FileUploadRootProps extends ChakraFileUpload.RootProps {
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const FileUploadRoot = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
FileUploadRootProps
|
||||
>(function FileUploadRoot(props, ref) {
|
||||
const { children, inputProps, ...rest } = props
|
||||
return (
|
||||
<ChakraFileUpload.Root {...rest}>
|
||||
<ChakraFileUpload.HiddenInput ref={ref} {...inputProps} />
|
||||
{children}
|
||||
</ChakraFileUpload.Root>
|
||||
)
|
||||
})
|
||||
|
||||
export interface FileUploadDropzoneProps
|
||||
extends ChakraFileUpload.DropzoneProps {
|
||||
label: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
}
|
||||
|
||||
export const FileUploadDropzone = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
FileUploadDropzoneProps
|
||||
>(function FileUploadDropzone(props, ref) {
|
||||
const { children, label, description, ...rest } = props
|
||||
return (
|
||||
<ChakraFileUpload.Dropzone ref={ref} {...rest}>
|
||||
<Icon fontSize="xl" color="fg.muted">
|
||||
<LuUpload />
|
||||
</Icon>
|
||||
<ChakraFileUpload.DropzoneContent>
|
||||
<div>{label}</div>
|
||||
{description && <Text color="fg.muted">{description}</Text>}
|
||||
</ChakraFileUpload.DropzoneContent>
|
||||
{children}
|
||||
</ChakraFileUpload.Dropzone>
|
||||
)
|
||||
})
|
||||
|
||||
interface VisibilityProps {
|
||||
showSize?: boolean
|
||||
clearable?: boolean
|
||||
}
|
||||
|
||||
interface FileUploadItemProps extends VisibilityProps {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileUploadItem = React.forwardRef<HTMLLIElement, FileUploadItemProps>(
|
||||
function FileUploadItem(props, ref) {
|
||||
const { file, showSize, clearable } = props
|
||||
return (
|
||||
<ChakraFileUpload.Item file={file} ref={ref}>
|
||||
<ChakraFileUpload.ItemPreview asChild>
|
||||
<Icon fontSize="lg" color="fg.muted">
|
||||
<LuFile />
|
||||
</Icon>
|
||||
</ChakraFileUpload.ItemPreview>
|
||||
|
||||
{showSize ? (
|
||||
<ChakraFileUpload.ItemContent>
|
||||
<ChakraFileUpload.ItemName />
|
||||
<ChakraFileUpload.ItemSizeText />
|
||||
</ChakraFileUpload.ItemContent>
|
||||
) : (
|
||||
<ChakraFileUpload.ItemName flex="1" />
|
||||
)}
|
||||
|
||||
{clearable && (
|
||||
<ChakraFileUpload.ItemDeleteTrigger asChild>
|
||||
<IconButton variant="ghost" color="fg.muted" size="xs">
|
||||
<LuX />
|
||||
</IconButton>
|
||||
</ChakraFileUpload.ItemDeleteTrigger>
|
||||
)}
|
||||
</ChakraFileUpload.Item>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface FileUploadListProps
|
||||
extends VisibilityProps,
|
||||
ChakraFileUpload.ItemGroupProps {
|
||||
files?: File[]
|
||||
}
|
||||
|
||||
export const FileUploadList = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
FileUploadListProps
|
||||
>(function FileUploadList(props, ref) {
|
||||
const { showSize, clearable, files, ...rest } = props
|
||||
|
||||
const fileUpload = useFileUploadContext()
|
||||
const acceptedFiles = files ?? fileUpload.acceptedFiles
|
||||
|
||||
if (acceptedFiles.length === 0) return null
|
||||
|
||||
return (
|
||||
<ChakraFileUpload.ItemGroup ref={ref} {...rest}>
|
||||
{acceptedFiles.map((file) => (
|
||||
<FileUploadItem
|
||||
key={file.name}
|
||||
file={file}
|
||||
showSize={showSize}
|
||||
clearable={clearable}
|
||||
/>
|
||||
))}
|
||||
</ChakraFileUpload.ItemGroup>
|
||||
)
|
||||
})
|
||||
|
||||
type Assign<T, U> = Omit<T, keyof U> & U
|
||||
|
||||
interface FileInputProps extends Assign<ButtonProps, RecipeProps<"input">> {
|
||||
placeholder?: React.ReactNode
|
||||
}
|
||||
|
||||
export const FileInput = React.forwardRef<HTMLButtonElement, FileInputProps>(
|
||||
function FileInput(props, ref) {
|
||||
const inputRecipe = useRecipe({ key: "input" })
|
||||
const [recipeProps, restProps] = inputRecipe.splitVariantProps(props)
|
||||
const { placeholder = "Select file(s)", ...rest } = restProps
|
||||
return (
|
||||
<ChakraFileUpload.Trigger asChild>
|
||||
<Button
|
||||
unstyled
|
||||
py="0"
|
||||
ref={ref}
|
||||
{...rest}
|
||||
css={[inputRecipe(recipeProps), props.css]}
|
||||
>
|
||||
<ChakraFileUpload.Context>
|
||||
{({ acceptedFiles }) => {
|
||||
if (acceptedFiles.length === 1) {
|
||||
return <span>{acceptedFiles[0].name}</span>
|
||||
}
|
||||
if (acceptedFiles.length > 1) {
|
||||
return <span>{acceptedFiles.length} files</span>
|
||||
}
|
||||
return <Span color="fg.subtle">{placeholder}</Span>
|
||||
}}
|
||||
</ChakraFileUpload.Context>
|
||||
</Button>
|
||||
</ChakraFileUpload.Trigger>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const FileUploadLabel = ChakraFileUpload.Label
|
||||
export const FileUploadClearTrigger = ChakraFileUpload.ClearTrigger
|
||||
export const FileUploadTrigger = ChakraFileUpload.Trigger
|
||||
36
src/components/ui/hover-card.tsx
Normal file
36
src/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { HoverCard, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface HoverCardContentProps extends HoverCard.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const HoverCardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
HoverCardContentProps
|
||||
>(function HoverCardContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<HoverCard.Positioner>
|
||||
<HoverCard.Content ref={ref} {...rest} />
|
||||
</HoverCard.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const HoverCardArrow = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
HoverCard.ArrowProps
|
||||
>(function HoverCardArrow(props, ref) {
|
||||
return (
|
||||
<HoverCard.Arrow ref={ref} {...props}>
|
||||
<HoverCard.ArrowTip />
|
||||
</HoverCard.Arrow>
|
||||
)
|
||||
})
|
||||
|
||||
export const HoverCardRoot = HoverCard.Root
|
||||
export const HoverCardTrigger = HoverCard.Trigger
|
||||
50
src/components/ui/input-group.tsx
Normal file
50
src/components/ui/input-group.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { BoxProps, InputElementProps } from "@chakra-ui/react"
|
||||
import { Group, InputElement } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface InputGroupProps extends BoxProps {
|
||||
startElementProps?: InputElementProps
|
||||
endElementProps?: InputElementProps
|
||||
startElement?: React.ReactNode
|
||||
endElement?: React.ReactNode
|
||||
children: React.ReactElement
|
||||
startOffset?: InputElementProps["paddingStart"]
|
||||
endOffset?: InputElementProps["paddingEnd"]
|
||||
}
|
||||
|
||||
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||
function InputGroup(props, ref) {
|
||||
const {
|
||||
startElement,
|
||||
startElementProps,
|
||||
endElement,
|
||||
endElementProps,
|
||||
children,
|
||||
startOffset = "6px",
|
||||
endOffset = "6px",
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Group ref={ref} {...rest}>
|
||||
{startElement && (
|
||||
<InputElement pointerEvents="none" {...startElementProps}>
|
||||
{startElement}
|
||||
</InputElement>
|
||||
)}
|
||||
{React.cloneElement(children, {
|
||||
...(startElement && {
|
||||
ps: `calc(var(--input-height) - ${startOffset})`,
|
||||
}),
|
||||
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
|
||||
...children.props,
|
||||
})}
|
||||
{endElement && (
|
||||
<InputElement placement="end" {...endElementProps}>
|
||||
{endElement}
|
||||
</InputElement>
|
||||
)}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
)
|
||||
12
src/components/ui/link-button.tsx
Normal file
12
src/components/ui/link-button.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import type { HTMLChakraProps, RecipeProps } from "@chakra-ui/react"
|
||||
import { createRecipeContext } from "@chakra-ui/react"
|
||||
|
||||
export interface LinkButtonProps
|
||||
extends HTMLChakraProps<"a", RecipeProps<"button">> {}
|
||||
|
||||
const { withContext } = createRecipeContext({ key: "button" })
|
||||
|
||||
// Replace "a" with your framework's link component
|
||||
export const LinkButton = withContext<HTMLAnchorElement, LinkButtonProps>("a")
|
||||
110
src/components/ui/menu.tsx
Normal file
110
src/components/ui/menu.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client"
|
||||
|
||||
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuCheck, LuChevronRight } from "react-icons/lu"
|
||||
|
||||
interface MenuContentProps extends ChakraMenu.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
|
||||
function MenuContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraMenu.Positioner>
|
||||
<ChakraMenu.Content ref={ref} {...rest} />
|
||||
</ChakraMenu.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const MenuArrow = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraMenu.ArrowProps
|
||||
>(function MenuArrow(props, ref) {
|
||||
return (
|
||||
<ChakraMenu.Arrow ref={ref} {...props}>
|
||||
<ChakraMenu.ArrowTip />
|
||||
</ChakraMenu.Arrow>
|
||||
)
|
||||
})
|
||||
|
||||
export const MenuCheckboxItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraMenu.CheckboxItemProps
|
||||
>(function MenuCheckboxItem(props, ref) {
|
||||
return (
|
||||
<ChakraMenu.CheckboxItem ref={ref} {...props}>
|
||||
<ChakraMenu.ItemIndicator hidden={false}>
|
||||
<LuCheck />
|
||||
</ChakraMenu.ItemIndicator>
|
||||
{props.children}
|
||||
</ChakraMenu.CheckboxItem>
|
||||
)
|
||||
})
|
||||
|
||||
export const MenuRadioItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraMenu.RadioItemProps
|
||||
>(function MenuRadioItem(props, ref) {
|
||||
const { children, ...rest } = props
|
||||
return (
|
||||
<ChakraMenu.RadioItem ps="8" ref={ref} {...rest}>
|
||||
<AbsoluteCenter axis="horizontal" left="4" asChild>
|
||||
<ChakraMenu.ItemIndicator>
|
||||
<LuCheck />
|
||||
</ChakraMenu.ItemIndicator>
|
||||
</AbsoluteCenter>
|
||||
<ChakraMenu.ItemText>{children}</ChakraMenu.ItemText>
|
||||
</ChakraMenu.RadioItem>
|
||||
)
|
||||
})
|
||||
|
||||
export const MenuItemGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraMenu.ItemGroupProps
|
||||
>(function MenuItemGroup(props, ref) {
|
||||
const { title, children, ...rest } = props
|
||||
return (
|
||||
<ChakraMenu.ItemGroup ref={ref} {...rest}>
|
||||
{title && (
|
||||
<ChakraMenu.ItemGroupLabel userSelect="none">
|
||||
{title}
|
||||
</ChakraMenu.ItemGroupLabel>
|
||||
)}
|
||||
{children}
|
||||
</ChakraMenu.ItemGroup>
|
||||
)
|
||||
})
|
||||
|
||||
export interface MenuTriggerItemProps extends ChakraMenu.ItemProps {
|
||||
startIcon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const MenuTriggerItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
MenuTriggerItemProps
|
||||
>(function MenuTriggerItem(props, ref) {
|
||||
const { startIcon, children, ...rest } = props
|
||||
return (
|
||||
<ChakraMenu.TriggerItem ref={ref} {...rest}>
|
||||
{startIcon}
|
||||
{children}
|
||||
<LuChevronRight />
|
||||
</ChakraMenu.TriggerItem>
|
||||
)
|
||||
})
|
||||
|
||||
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup
|
||||
export const MenuContextTrigger = ChakraMenu.ContextTrigger
|
||||
export const MenuRoot = ChakraMenu.Root
|
||||
export const MenuSeparator = ChakraMenu.Separator
|
||||
|
||||
export const MenuItem = ChakraMenu.Item
|
||||
export const MenuItemText = ChakraMenu.ItemText
|
||||
export const MenuItemCommand = ChakraMenu.ItemCommand
|
||||
export const MenuTrigger = ChakraMenu.Trigger
|
||||
57
src/components/ui/native-select.tsx
Normal file
57
src/components/ui/native-select.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import { NativeSelect as Select } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface NativeSelectRootProps extends Select.RootProps {
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const NativeSelectRoot = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
NativeSelectRootProps
|
||||
>(function NativeSelect(props, ref) {
|
||||
const { icon, children, ...rest } = props
|
||||
return (
|
||||
<Select.Root ref={ref} {...rest}>
|
||||
{children}
|
||||
<Select.Indicator>{icon}</Select.Indicator>
|
||||
</Select.Root>
|
||||
)
|
||||
})
|
||||
|
||||
interface NativeSelectItem {
|
||||
value: string
|
||||
label: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface NativeSelectField extends Select.FieldProps {
|
||||
items?: Array<string | NativeSelectItem>
|
||||
}
|
||||
|
||||
export const NativeSelectField = React.forwardRef<
|
||||
HTMLSelectElement,
|
||||
NativeSelectField
|
||||
>(function NativeSelectField(props, ref) {
|
||||
const { items: itemsProp, children, ...rest } = props
|
||||
|
||||
const items = React.useMemo(
|
||||
() =>
|
||||
itemsProp?.map((item) =>
|
||||
typeof item === "string" ? { label: item, value: item } : item,
|
||||
),
|
||||
[itemsProp],
|
||||
)
|
||||
|
||||
return (
|
||||
<Select.Field ref={ref} {...rest}>
|
||||
{children}
|
||||
{items?.map((item) => (
|
||||
<option key={item.value} value={item.value} disabled={item.disabled}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select.Field>
|
||||
)
|
||||
})
|
||||
24
src/components/ui/number-input.tsx
Normal file
24
src/components/ui/number-input.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NumberInput as ChakraNumberInput } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface NumberInputProps extends ChakraNumberInput.RootProps {}
|
||||
|
||||
export const NumberInputRoot = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
NumberInputProps
|
||||
>(function NumberInput(props, ref) {
|
||||
const { children, ...rest } = props
|
||||
return (
|
||||
<ChakraNumberInput.Root ref={ref} variant="outline" {...rest}>
|
||||
{children}
|
||||
<ChakraNumberInput.Control>
|
||||
<ChakraNumberInput.IncrementTrigger />
|
||||
<ChakraNumberInput.DecrementTrigger />
|
||||
</ChakraNumberInput.Control>
|
||||
</ChakraNumberInput.Root>
|
||||
)
|
||||
})
|
||||
|
||||
export const NumberInputField = ChakraNumberInput.Input
|
||||
export const NumberInputScruber = ChakraNumberInput.Scrubber
|
||||
export const NumberInputLabel = ChakraNumberInput.Label
|
||||
208
src/components/ui/pagination.tsx
Normal file
208
src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
"use client"
|
||||
|
||||
import type { ButtonProps, TextProps } from "@chakra-ui/react"
|
||||
import {
|
||||
Button,
|
||||
Pagination as ChakraPagination,
|
||||
IconButton,
|
||||
Text,
|
||||
createContext,
|
||||
usePaginationContext,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import {
|
||||
HiChevronLeft,
|
||||
HiChevronRight,
|
||||
HiMiniEllipsisHorizontal,
|
||||
} from "react-icons/hi2"
|
||||
import { LinkButton } from "./link-button"
|
||||
|
||||
interface ButtonVariantMap {
|
||||
current: ButtonProps["variant"]
|
||||
default: ButtonProps["variant"]
|
||||
ellipsis: ButtonProps["variant"]
|
||||
}
|
||||
|
||||
type PaginationVariant = "outline" | "solid" | "subtle"
|
||||
|
||||
interface ButtonVariantContext {
|
||||
size: ButtonProps["size"]
|
||||
variantMap: ButtonVariantMap
|
||||
getHref?: (page: number) => string
|
||||
}
|
||||
|
||||
const [RootPropsProvider, useRootProps] = createContext<ButtonVariantContext>({
|
||||
name: "RootPropsProvider",
|
||||
})
|
||||
|
||||
export interface PaginationRootProps
|
||||
extends Omit<ChakraPagination.RootProps, "type"> {
|
||||
size?: ButtonProps["size"]
|
||||
variant?: PaginationVariant
|
||||
getHref?: (page: number) => string
|
||||
}
|
||||
|
||||
const variantMap: Record<PaginationVariant, ButtonVariantMap> = {
|
||||
outline: { default: "ghost", ellipsis: "plain", current: "outline" },
|
||||
solid: { default: "outline", ellipsis: "outline", current: "solid" },
|
||||
subtle: { default: "ghost", ellipsis: "plain", current: "subtle" },
|
||||
}
|
||||
|
||||
export const PaginationRoot = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PaginationRootProps
|
||||
>(function PaginationRoot(props, ref) {
|
||||
const { size = "sm", variant = "outline", getHref, ...rest } = props
|
||||
return (
|
||||
<RootPropsProvider
|
||||
value={{ size, variantMap: variantMap[variant], getHref }}
|
||||
>
|
||||
<ChakraPagination.Root
|
||||
ref={ref}
|
||||
type={getHref ? "link" : "button"}
|
||||
{...rest}
|
||||
/>
|
||||
</RootPropsProvider>
|
||||
)
|
||||
})
|
||||
|
||||
export const PaginationEllipsis = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraPagination.EllipsisProps
|
||||
>(function PaginationEllipsis(props, ref) {
|
||||
const { size, variantMap } = useRootProps()
|
||||
return (
|
||||
<ChakraPagination.Ellipsis ref={ref} {...props} asChild>
|
||||
<Button as="span" variant={variantMap.ellipsis} size={size}>
|
||||
<HiMiniEllipsisHorizontal />
|
||||
</Button>
|
||||
</ChakraPagination.Ellipsis>
|
||||
)
|
||||
})
|
||||
|
||||
export const PaginationItem = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraPagination.ItemProps
|
||||
>(function PaginationItem(props, ref) {
|
||||
const { page } = usePaginationContext()
|
||||
const { size, variantMap, getHref } = useRootProps()
|
||||
|
||||
const current = page === props.value
|
||||
const variant = current ? variantMap.current : variantMap.default
|
||||
|
||||
if (getHref) {
|
||||
return (
|
||||
<LinkButton href={getHref(props.value)} variant={variant} size={size}>
|
||||
{props.value}
|
||||
</LinkButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChakraPagination.Item ref={ref} {...props} asChild>
|
||||
<Button variant={variant} size={size}>
|
||||
{props.value}
|
||||
</Button>
|
||||
</ChakraPagination.Item>
|
||||
)
|
||||
})
|
||||
|
||||
export const PaginationPrevTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraPagination.PrevTriggerProps
|
||||
>(function PaginationPrevTrigger(props, ref) {
|
||||
const { size, variantMap, getHref } = useRootProps()
|
||||
const { previousPage } = usePaginationContext()
|
||||
|
||||
if (getHref) {
|
||||
return (
|
||||
<LinkButton
|
||||
href={previousPage != null ? getHref(previousPage) : undefined}
|
||||
variant={variantMap.default}
|
||||
size={size}
|
||||
>
|
||||
<HiChevronLeft />
|
||||
</LinkButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChakraPagination.PrevTrigger ref={ref} asChild {...props}>
|
||||
<IconButton variant={variantMap.default} size={size}>
|
||||
<HiChevronLeft />
|
||||
</IconButton>
|
||||
</ChakraPagination.PrevTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const PaginationNextTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraPagination.NextTriggerProps
|
||||
>(function PaginationNextTrigger(props, ref) {
|
||||
const { size, variantMap, getHref } = useRootProps()
|
||||
const { nextPage } = usePaginationContext()
|
||||
|
||||
if (getHref) {
|
||||
return (
|
||||
<LinkButton
|
||||
href={nextPage != null ? getHref(nextPage) : undefined}
|
||||
variant={variantMap.default}
|
||||
size={size}
|
||||
>
|
||||
<HiChevronRight />
|
||||
</LinkButton>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChakraPagination.NextTrigger ref={ref} asChild {...props}>
|
||||
<IconButton variant={variantMap.default} size={size}>
|
||||
<HiChevronRight />
|
||||
</IconButton>
|
||||
</ChakraPagination.NextTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const PaginationItems = (props: React.HTMLAttributes<HTMLElement>) => {
|
||||
return (
|
||||
<ChakraPagination.Context>
|
||||
{({ pages }) =>
|
||||
pages.map((page, index) => {
|
||||
return page.type === "ellipsis" ? (
|
||||
<PaginationEllipsis key={index} index={index} {...props} />
|
||||
) : (
|
||||
<PaginationItem
|
||||
key={index}
|
||||
type="page"
|
||||
value={page.value}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ChakraPagination.Context>
|
||||
)
|
||||
}
|
||||
|
||||
interface PageTextProps extends TextProps {
|
||||
format?: "short" | "compact" | "long"
|
||||
}
|
||||
|
||||
export const PaginationPageText = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
PageTextProps
|
||||
>(function PaginationPageText(props, ref) {
|
||||
const { format = "compact", ...rest } = props
|
||||
const { page, totalPages, pageRange, count } = usePaginationContext()
|
||||
const content = React.useMemo(() => {
|
||||
if (format === "short") return `${page} / ${totalPages}`
|
||||
if (format === "compact") return `${page} of ${totalPages}`
|
||||
return `${pageRange.start + 1} - ${pageRange.end} of ${count}`
|
||||
}, [format, page, totalPages, pageRange, count])
|
||||
|
||||
return (
|
||||
<Text fontWeight="medium" ref={ref} {...rest}>
|
||||
{content}
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
148
src/components/ui/password-input.tsx
Normal file
148
src/components/ui/password-input.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client"
|
||||
|
||||
import type {
|
||||
ButtonProps,
|
||||
GroupProps,
|
||||
InputProps,
|
||||
StackProps,
|
||||
} from "@chakra-ui/react"
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
IconButton,
|
||||
Input,
|
||||
Stack,
|
||||
mergeRefs,
|
||||
useControllableState,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuEye, LuEyeOff } from "react-icons/lu"
|
||||
import { InputGroup } from "./input-group"
|
||||
|
||||
export interface PasswordVisibilityProps {
|
||||
defaultVisible?: boolean
|
||||
visible?: boolean
|
||||
onVisibleChange?: (visible: boolean) => void
|
||||
visibilityIcon?: { on: React.ReactNode; off: React.ReactNode }
|
||||
}
|
||||
|
||||
export interface PasswordInputProps
|
||||
extends InputProps,
|
||||
PasswordVisibilityProps {
|
||||
rootProps?: GroupProps
|
||||
}
|
||||
|
||||
export const PasswordInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
PasswordInputProps
|
||||
>(function PasswordInput(props, ref) {
|
||||
const {
|
||||
rootProps,
|
||||
defaultVisible,
|
||||
visible: visibleProp,
|
||||
onVisibleChange,
|
||||
visibilityIcon = { on: <LuEye />, off: <LuEyeOff /> },
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const [visible, setVisible] = useControllableState({
|
||||
value: visibleProp,
|
||||
defaultValue: defaultVisible || false,
|
||||
onChange: onVisibleChange,
|
||||
})
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<InputGroup
|
||||
width="full"
|
||||
endElement={
|
||||
<VisibilityTrigger
|
||||
disabled={rest.disabled}
|
||||
onPointerDown={(e) => {
|
||||
if (rest.disabled) return
|
||||
if (e.button !== 0) return
|
||||
e.preventDefault()
|
||||
setVisible(!visible)
|
||||
}}
|
||||
>
|
||||
{visible ? visibilityIcon.off : visibilityIcon.on}
|
||||
</VisibilityTrigger>
|
||||
}
|
||||
{...rootProps}
|
||||
>
|
||||
<Input
|
||||
{...rest}
|
||||
ref={mergeRefs(ref, inputRef)}
|
||||
type={visible ? "text" : "password"}
|
||||
/>
|
||||
</InputGroup>
|
||||
)
|
||||
})
|
||||
|
||||
const VisibilityTrigger = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function VisibilityTrigger(props, ref) {
|
||||
return (
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
ref={ref}
|
||||
me="-2"
|
||||
aspectRatio="square"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
height="calc(100% - {spacing.2})"
|
||||
aria-label="Toggle password visibility"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface PasswordStrengthMeterProps extends StackProps {
|
||||
max?: number
|
||||
value: number
|
||||
}
|
||||
|
||||
export const PasswordStrengthMeter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PasswordStrengthMeterProps
|
||||
>(function PasswordStrengthMeter(props, ref) {
|
||||
const { max = 4, value, ...rest } = props
|
||||
|
||||
const percent = (value / max) * 100
|
||||
const { label, colorPalette } = getColorPalette(percent)
|
||||
|
||||
return (
|
||||
<Stack align="flex-end" gap="1" ref={ref} {...rest}>
|
||||
<HStack width="full" ref={ref} {...rest}>
|
||||
{Array.from({ length: max }).map((_, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
height="1"
|
||||
flex="1"
|
||||
rounded="sm"
|
||||
data-selected={index < value ? "" : undefined}
|
||||
layerStyle="fill.subtle"
|
||||
colorPalette="gray"
|
||||
_selected={{
|
||||
colorPalette,
|
||||
layerStyle: "fill.solid",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
{label && <HStack textStyle="xs">{label}</HStack>}
|
||||
</Stack>
|
||||
)
|
||||
})
|
||||
|
||||
function getColorPalette(percent: number) {
|
||||
switch (true) {
|
||||
case percent < 33:
|
||||
return { label: "Low", colorPalette: "red" }
|
||||
case percent < 66:
|
||||
return { label: "Medium", colorPalette: "orange" }
|
||||
default:
|
||||
return { label: "High", colorPalette: "green" }
|
||||
}
|
||||
}
|
||||
27
src/components/ui/pin-input.tsx
Normal file
27
src/components/ui/pin-input.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { PinInput as ChakraPinInput, Group } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface PinInputProps extends ChakraPinInput.RootProps {
|
||||
rootRef?: React.Ref<HTMLDivElement>
|
||||
count?: number
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
attached?: boolean
|
||||
}
|
||||
|
||||
export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
|
||||
function PinInput(props, ref) {
|
||||
const { count = 4, inputProps, rootRef, attached, ...rest } = props
|
||||
return (
|
||||
<ChakraPinInput.Root ref={rootRef} {...rest}>
|
||||
<ChakraPinInput.HiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraPinInput.Control>
|
||||
<Group attached={attached}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<ChakraPinInput.Input key={index} index={index} />
|
||||
))}
|
||||
</Group>
|
||||
</ChakraPinInput.Control>
|
||||
</ChakraPinInput.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
59
src/components/ui/popover.tsx
Normal file
59
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Popover as ChakraPopover, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface PopoverContentProps extends ChakraPopover.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const PopoverContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PopoverContentProps
|
||||
>(function PopoverContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraPopover.Positioner>
|
||||
<ChakraPopover.Content ref={ref} {...rest} />
|
||||
</ChakraPopover.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverArrow = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraPopover.ArrowProps
|
||||
>(function PopoverArrow(props, ref) {
|
||||
return (
|
||||
<ChakraPopover.Arrow {...props} ref={ref}>
|
||||
<ChakraPopover.ArrowTip />
|
||||
</ChakraPopover.Arrow>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraPopover.CloseTriggerProps
|
||||
>(function PopoverCloseTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraPopover.CloseTrigger
|
||||
position="absolute"
|
||||
top="1"
|
||||
insetEnd="1"
|
||||
{...props}
|
||||
asChild
|
||||
ref={ref}
|
||||
>
|
||||
<CloseButton size="sm" />
|
||||
</ChakraPopover.CloseTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
export const PopoverTitle = ChakraPopover.Title
|
||||
export const PopoverDescription = ChakraPopover.Description
|
||||
export const PopoverFooter = ChakraPopover.Footer
|
||||
export const PopoverHeader = ChakraPopover.Header
|
||||
export const PopoverRoot = ChakraPopover.Root
|
||||
export const PopoverBody = ChakraPopover.Body
|
||||
export const PopoverTrigger = ChakraPopover.Trigger
|
||||
37
src/components/ui/progress-circle.tsx
Normal file
37
src/components/ui/progress-circle.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { SystemStyleObject } from "@chakra-ui/react"
|
||||
import {
|
||||
AbsoluteCenter,
|
||||
ProgressCircle as ChakraProgressCircle,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps {
|
||||
trackColor?: SystemStyleObject["stroke"]
|
||||
cap?: SystemStyleObject["strokeLinecap"]
|
||||
}
|
||||
|
||||
export const ProgressCircleRing = React.forwardRef<
|
||||
SVGSVGElement,
|
||||
ProgressCircleRingProps
|
||||
>(function ProgressCircleRing(props, ref) {
|
||||
const { trackColor, cap, color, ...rest } = props
|
||||
return (
|
||||
<ChakraProgressCircle.Circle {...rest} ref={ref}>
|
||||
<ChakraProgressCircle.Track stroke={trackColor} />
|
||||
<ChakraProgressCircle.Range stroke={color} strokeLinecap={cap} />
|
||||
</ChakraProgressCircle.Circle>
|
||||
)
|
||||
})
|
||||
|
||||
export const ProgressCircleValueText = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraProgressCircle.ValueTextProps
|
||||
>(function ProgressCircleValueText(props, ref) {
|
||||
return (
|
||||
<AbsoluteCenter>
|
||||
<ChakraProgressCircle.ValueText {...props} ref={ref} />
|
||||
</AbsoluteCenter>
|
||||
)
|
||||
})
|
||||
|
||||
export const ProgressCircleRoot = ChakraProgressCircle.Root
|
||||
34
src/components/ui/progress.tsx
Normal file
34
src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Progress as ChakraProgress } from "@chakra-ui/react"
|
||||
import { InfoTip } from "./toggle-tip"
|
||||
import * as React from "react"
|
||||
|
||||
export const ProgressBar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraProgress.TrackProps
|
||||
>(function ProgressBar(props, ref) {
|
||||
return (
|
||||
<ChakraProgress.Track {...props} ref={ref}>
|
||||
<ChakraProgress.Range />
|
||||
</ChakraProgress.Track>
|
||||
)
|
||||
})
|
||||
|
||||
export interface ProgressLabelProps extends ChakraProgress.LabelProps {
|
||||
info?: React.ReactNode
|
||||
}
|
||||
|
||||
export const ProgressLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ProgressLabelProps
|
||||
>(function ProgressLabel(props, ref) {
|
||||
const { children, info, ...rest } = props
|
||||
return (
|
||||
<ChakraProgress.Label {...rest} ref={ref}>
|
||||
{children}
|
||||
{info && <InfoTip>{info}</InfoTip>}
|
||||
</ChakraProgress.Label>
|
||||
)
|
||||
})
|
||||
|
||||
export const ProgressRoot = ChakraProgress.Root
|
||||
export const ProgressValueText = ChakraProgress.ValueText
|
||||
264
src/components/ui/prose.tsx
Normal file
264
src/components/ui/prose.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
"use client"
|
||||
|
||||
import { chakra } from "@chakra-ui/react"
|
||||
|
||||
export const Prose = chakra("div", {
|
||||
base: {
|
||||
color: "fg.muted",
|
||||
maxWidth: "65ch",
|
||||
fontSize: "sm",
|
||||
lineHeight: "1.7em",
|
||||
"& p": {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em",
|
||||
},
|
||||
"& blockquote": {
|
||||
marginTop: "1.285em",
|
||||
marginBottom: "1.285em",
|
||||
paddingInline: "1.285em",
|
||||
borderInlineStartWidth: "0.25em",
|
||||
},
|
||||
"& a": {
|
||||
color: "fg",
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: "3px",
|
||||
textDecorationThickness: "2px",
|
||||
textDecorationColor: "border.muted",
|
||||
fontWeight: "500",
|
||||
},
|
||||
"& strong": {
|
||||
fontWeight: "600",
|
||||
},
|
||||
"& a strong": {
|
||||
color: "inherit",
|
||||
},
|
||||
"& h1": {
|
||||
fontSize: "2.15em",
|
||||
letterSpacing: "-0.02em",
|
||||
marginTop: "0",
|
||||
marginBottom: "0.8em",
|
||||
lineHeight: "1.2em",
|
||||
},
|
||||
"& h2": {
|
||||
fontSize: "1.4em",
|
||||
letterSpacing: "-0.02em",
|
||||
marginTop: "1.6em",
|
||||
marginBottom: "0.8em",
|
||||
lineHeight: "1.4em",
|
||||
},
|
||||
"& h3": {
|
||||
fontSize: "1.285em",
|
||||
letterSpacing: "-0.01em",
|
||||
marginTop: "1.5em",
|
||||
marginBottom: "0.4em",
|
||||
lineHeight: "1.5em",
|
||||
},
|
||||
"& h4": {
|
||||
marginTop: "1.4em",
|
||||
marginBottom: "0.5em",
|
||||
letterSpacing: "-0.01em",
|
||||
lineHeight: "1.5em",
|
||||
},
|
||||
"& img": {
|
||||
marginTop: "1.7em",
|
||||
marginBottom: "1.7em",
|
||||
borderRadius: "lg",
|
||||
boxShadow: "inset",
|
||||
},
|
||||
"& picture": {
|
||||
marginTop: "1.7em",
|
||||
marginBottom: "1.7em",
|
||||
},
|
||||
"& picture > img": {
|
||||
marginTop: "0",
|
||||
marginBottom: "0",
|
||||
},
|
||||
"& video": {
|
||||
marginTop: "1.7em",
|
||||
marginBottom: "1.7em",
|
||||
},
|
||||
"& kbd": {
|
||||
fontSize: "0.85em",
|
||||
borderRadius: "xs",
|
||||
paddingTop: "0.15em",
|
||||
paddingBottom: "0.15em",
|
||||
paddingInlineEnd: "0.35em",
|
||||
paddingInlineStart: "0.35em",
|
||||
fontFamily: "inherit",
|
||||
color: "fg.muted",
|
||||
"--shadow": "colors.border",
|
||||
boxShadow: "0 0 0 1px var(--shadow),0 1px 0 1px var(--shadow)",
|
||||
},
|
||||
"& code": {
|
||||
fontSize: "0.925em",
|
||||
letterSpacing: "-0.01em",
|
||||
borderRadius: "md",
|
||||
borderWidth: "1px",
|
||||
padding: "0.25em",
|
||||
},
|
||||
"& pre code": {
|
||||
fontSize: "inherit",
|
||||
letterSpacing: "inherit",
|
||||
borderWidth: "inherit",
|
||||
padding: "0",
|
||||
},
|
||||
"& h2 code": {
|
||||
fontSize: "0.9em",
|
||||
},
|
||||
"& h3 code": {
|
||||
fontSize: "0.8em",
|
||||
},
|
||||
"& pre": {
|
||||
backgroundColor: "bg.subtle",
|
||||
marginTop: "1.6em",
|
||||
marginBottom: "1.6em",
|
||||
borderRadius: "md",
|
||||
fontSize: "0.9em",
|
||||
paddingTop: "0.65em",
|
||||
paddingBottom: "0.65em",
|
||||
paddingInlineEnd: "1em",
|
||||
paddingInlineStart: "1em",
|
||||
overflowX: "auto",
|
||||
fontWeight: "400",
|
||||
},
|
||||
"& ol": {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em",
|
||||
paddingInlineStart: "1.5em",
|
||||
},
|
||||
"& ul": {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em",
|
||||
paddingInlineStart: "1.5em",
|
||||
},
|
||||
"& li": {
|
||||
marginTop: "0.285em",
|
||||
marginBottom: "0.285em",
|
||||
},
|
||||
"& ol > li": {
|
||||
paddingInlineStart: "0.4em",
|
||||
listStyleType: "decimal",
|
||||
"&::marker": {
|
||||
color: "fg.muted",
|
||||
},
|
||||
},
|
||||
"& ul > li": {
|
||||
paddingInlineStart: "0.4em",
|
||||
listStyleType: "disc",
|
||||
"&::marker": {
|
||||
color: "fg.muted",
|
||||
},
|
||||
},
|
||||
"& > ul > li p": {
|
||||
marginTop: "0.5em",
|
||||
marginBottom: "0.5em",
|
||||
},
|
||||
"& > ul > li > p:first-of-type": {
|
||||
marginTop: "1em",
|
||||
},
|
||||
"& > ul > li > p:last-of-type": {
|
||||
marginBottom: "1em",
|
||||
},
|
||||
"& > ol > li > p:first-of-type": {
|
||||
marginTop: "1em",
|
||||
},
|
||||
"& > ol > li > p:last-of-type": {
|
||||
marginBottom: "1em",
|
||||
},
|
||||
"& ul ul, ul ol, ol ul, ol ol": {
|
||||
marginTop: "0.5em",
|
||||
marginBottom: "0.5em",
|
||||
},
|
||||
"& dl": {
|
||||
marginTop: "1em",
|
||||
marginBottom: "1em",
|
||||
},
|
||||
"& dt": {
|
||||
fontWeight: "600",
|
||||
marginTop: "1em",
|
||||
},
|
||||
"& dd": {
|
||||
marginTop: "0.285em",
|
||||
paddingInlineStart: "1.5em",
|
||||
},
|
||||
"& hr": {
|
||||
marginTop: "2.25em",
|
||||
marginBottom: "2.25em",
|
||||
},
|
||||
"& :is(h1,h2,h3,h4,h5,hr) + *": {
|
||||
marginTop: "0",
|
||||
},
|
||||
"& table": {
|
||||
width: "100%",
|
||||
tableLayout: "auto",
|
||||
textAlign: "start",
|
||||
lineHeight: "1.5em",
|
||||
marginTop: "2em",
|
||||
marginBottom: "2em",
|
||||
},
|
||||
"& thead": {
|
||||
borderBottomWidth: "1px",
|
||||
color: "fg",
|
||||
},
|
||||
"& tbody tr": {
|
||||
borderBottomWidth: "1px",
|
||||
borderBottomColor: "border",
|
||||
},
|
||||
"& thead th": {
|
||||
paddingInlineEnd: "1em",
|
||||
paddingBottom: "0.65em",
|
||||
paddingInlineStart: "1em",
|
||||
fontWeight: "medium",
|
||||
textAlign: "start",
|
||||
},
|
||||
"& thead th:first-of-type": {
|
||||
paddingInlineStart: "0",
|
||||
},
|
||||
"& thead th:last-of-type": {
|
||||
paddingInlineEnd: "0",
|
||||
},
|
||||
"& tbody td, tfoot td": {
|
||||
paddingTop: "0.65em",
|
||||
paddingInlineEnd: "1em",
|
||||
paddingBottom: "0.65em",
|
||||
paddingInlineStart: "1em",
|
||||
},
|
||||
"& tbody td:first-of-type, tfoot td:first-of-type": {
|
||||
paddingInlineStart: "0",
|
||||
},
|
||||
"& tbody td:last-of-type, tfoot td:last-of-type": {
|
||||
paddingInlineEnd: "0",
|
||||
},
|
||||
"& figure": {
|
||||
marginTop: "1.625em",
|
||||
marginBottom: "1.625em",
|
||||
},
|
||||
"& figure > *": {
|
||||
marginTop: "0",
|
||||
marginBottom: "0",
|
||||
},
|
||||
"& figcaption": {
|
||||
fontSize: "0.85em",
|
||||
lineHeight: "1.25em",
|
||||
marginTop: "0.85em",
|
||||
color: "fg.muted",
|
||||
},
|
||||
"& h1, h2, h3, h4": {
|
||||
color: "fg",
|
||||
fontWeight: "600",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
md: {
|
||||
fontSize: "sm",
|
||||
},
|
||||
lg: {
|
||||
fontSize: "md",
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
})
|
||||
15
src/components/ui/provider.tsx
Normal file
15
src/components/ui/provider.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||
import {
|
||||
ColorModeProvider,
|
||||
type ColorModeProviderProps,
|
||||
} from "./color-mode"
|
||||
|
||||
export function Provider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
||||
58
src/components/ui/radio-card.tsx
Normal file
58
src/components/ui/radio-card.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { RadioCard } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface RadioCardItemProps extends RadioCard.ItemProps {
|
||||
icon?: React.ReactElement
|
||||
label?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
addon?: React.ReactNode
|
||||
indicator?: React.ReactNode | null
|
||||
indicatorPlacement?: "start" | "end" | "inside"
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const RadioCardItem = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
RadioCardItemProps
|
||||
>(function RadioCardItem(props, ref) {
|
||||
const {
|
||||
inputProps,
|
||||
label,
|
||||
description,
|
||||
addon,
|
||||
icon,
|
||||
indicator = <RadioCard.ItemIndicator />,
|
||||
indicatorPlacement = "end",
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const hasContent = label || description || icon
|
||||
const ContentWrapper = indicator ? RadioCard.ItemContent : React.Fragment
|
||||
|
||||
return (
|
||||
<RadioCard.Item {...rest}>
|
||||
<RadioCard.ItemHiddenInput ref={ref} {...inputProps} />
|
||||
<RadioCard.ItemControl>
|
||||
{indicatorPlacement === "start" && indicator}
|
||||
{hasContent && (
|
||||
<ContentWrapper>
|
||||
{icon}
|
||||
{label && <RadioCard.ItemText>{label}</RadioCard.ItemText>}
|
||||
{description && (
|
||||
<RadioCard.ItemDescription>
|
||||
{description}
|
||||
</RadioCard.ItemDescription>
|
||||
)}
|
||||
{indicatorPlacement === "inside" && indicator}
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{indicatorPlacement === "end" && indicator}
|
||||
</RadioCard.ItemControl>
|
||||
{addon && <RadioCard.ItemAddon>{addon}</RadioCard.ItemAddon>}
|
||||
</RadioCard.Item>
|
||||
)
|
||||
})
|
||||
|
||||
export const RadioCardRoot = RadioCard.Root
|
||||
export const RadioCardLabel = RadioCard.Label
|
||||
export const RadioCardItemIndicator = RadioCard.ItemIndicator
|
||||
24
src/components/ui/radio.tsx
Normal file
24
src/components/ui/radio.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface RadioProps extends ChakraRadioGroup.ItemProps {
|
||||
rootRef?: React.Ref<HTMLDivElement>
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||
function Radio(props, ref) {
|
||||
const { children, inputProps, rootRef, ...rest } = props
|
||||
return (
|
||||
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
|
||||
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraRadioGroup.ItemIndicator />
|
||||
{children && (
|
||||
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
|
||||
)}
|
||||
</ChakraRadioGroup.Item>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const RadioGroup = ChakraRadioGroup.Root
|
||||
27
src/components/ui/rating.tsx
Normal file
27
src/components/ui/rating.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { RatingGroup } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface RatingProps extends RatingGroup.RootProps {
|
||||
icon?: React.ReactElement
|
||||
count?: number
|
||||
label?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
|
||||
function Rating(props, ref) {
|
||||
const { icon, count = 5, label, ...rest } = props
|
||||
return (
|
||||
<RatingGroup.Root ref={ref} count={count} {...rest}>
|
||||
{label && <RatingGroup.Label>{label}</RatingGroup.Label>}
|
||||
<RatingGroup.HiddenInput />
|
||||
<RatingGroup.Control>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<RatingGroup.Item key={index} index={index + 1}>
|
||||
<RatingGroup.ItemIndicator icon={icon} />
|
||||
</RatingGroup.Item>
|
||||
))}
|
||||
</RatingGroup.Control>
|
||||
</RatingGroup.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
47
src/components/ui/segmented-control.tsx
Normal file
47
src/components/ui/segmented-control.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import { For, SegmentGroup } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface Item {
|
||||
value: string
|
||||
label: React.ReactNode
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface SegmentedControlProps extends SegmentGroup.RootProps {
|
||||
items: Array<string | Item>
|
||||
}
|
||||
|
||||
function normalize(items: Array<string | Item>): Item[] {
|
||||
return items.map((item) => {
|
||||
if (typeof item === "string") return { value: item, label: item }
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
export const SegmentedControl = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
SegmentedControlProps
|
||||
>(function SegmentedControl(props, ref) {
|
||||
const { items, ...rest } = props
|
||||
const data = React.useMemo(() => normalize(items), [items])
|
||||
|
||||
return (
|
||||
<SegmentGroup.Root ref={ref} {...rest}>
|
||||
<SegmentGroup.Indicator />
|
||||
<For each={data}>
|
||||
{(item) => (
|
||||
<SegmentGroup.Item
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<SegmentGroup.ItemText>{item.label}</SegmentGroup.ItemText>
|
||||
<SegmentGroup.ItemHiddenInput />
|
||||
</SegmentGroup.Item>
|
||||
)}
|
||||
</For>
|
||||
</SegmentGroup.Root>
|
||||
)
|
||||
})
|
||||
143
src/components/ui/select.tsx
Normal file
143
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import type { CollectionItem } from "@chakra-ui/react"
|
||||
import { Select as ChakraSelect, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
|
||||
interface SelectTriggerProps extends ChakraSelect.ControlProps {
|
||||
clearable?: boolean
|
||||
}
|
||||
|
||||
export const SelectTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
SelectTriggerProps
|
||||
>(function SelectTrigger(props, ref) {
|
||||
const { children, clearable, ...rest } = props
|
||||
return (
|
||||
<ChakraSelect.Control {...rest}>
|
||||
<ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
|
||||
<ChakraSelect.IndicatorGroup>
|
||||
{clearable && <SelectClearTrigger />}
|
||||
<ChakraSelect.Indicator />
|
||||
</ChakraSelect.IndicatorGroup>
|
||||
</ChakraSelect.Control>
|
||||
)
|
||||
})
|
||||
|
||||
const SelectClearTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChakraSelect.ClearTriggerProps
|
||||
>(function SelectClearTrigger(props, ref) {
|
||||
return (
|
||||
<ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
|
||||
<CloseButton
|
||||
size="xs"
|
||||
variant="plain"
|
||||
focusVisibleRing="inside"
|
||||
focusRingWidth="2px"
|
||||
pointerEvents="auto"
|
||||
/>
|
||||
</ChakraSelect.ClearTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
interface SelectContentProps extends ChakraSelect.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const SelectContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
SelectContentProps
|
||||
>(function SelectContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraSelect.Positioner>
|
||||
<ChakraSelect.Content {...rest} ref={ref} />
|
||||
</ChakraSelect.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export const SelectItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraSelect.ItemProps
|
||||
>(function SelectItem(props, ref) {
|
||||
const { item, children, ...rest } = props
|
||||
return (
|
||||
<ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
|
||||
{children}
|
||||
<ChakraSelect.ItemIndicator />
|
||||
</ChakraSelect.Item>
|
||||
)
|
||||
})
|
||||
|
||||
interface SelectValueTextProps
|
||||
extends Omit<ChakraSelect.ValueTextProps, "children"> {
|
||||
children?(items: CollectionItem[]): React.ReactNode
|
||||
}
|
||||
|
||||
export const SelectValueText = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
SelectValueTextProps
|
||||
>(function SelectValueText(props, ref) {
|
||||
const { children, ...rest } = props
|
||||
return (
|
||||
<ChakraSelect.ValueText {...rest} ref={ref}>
|
||||
<ChakraSelect.Context>
|
||||
{(select) => {
|
||||
const items = select.selectedItems
|
||||
if (items.length === 0) return props.placeholder
|
||||
if (children) return children(items)
|
||||
if (items.length === 1)
|
||||
return select.collection.stringifyItem(items[0])
|
||||
return `${items.length} selected`
|
||||
}}
|
||||
</ChakraSelect.Context>
|
||||
</ChakraSelect.ValueText>
|
||||
)
|
||||
})
|
||||
|
||||
export const SelectRoot = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraSelect.RootProps
|
||||
>(function SelectRoot(props, ref) {
|
||||
return (
|
||||
<ChakraSelect.Root
|
||||
{...props}
|
||||
ref={ref}
|
||||
positioning={{ sameWidth: true, ...props.positioning }}
|
||||
>
|
||||
{props.asChild ? (
|
||||
props.children
|
||||
) : (
|
||||
<>
|
||||
<ChakraSelect.HiddenSelect />
|
||||
{props.children}
|
||||
</>
|
||||
)}
|
||||
</ChakraSelect.Root>
|
||||
)
|
||||
}) as ChakraSelect.RootComponent
|
||||
|
||||
interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps {
|
||||
label: React.ReactNode
|
||||
}
|
||||
|
||||
export const SelectItemGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
SelectItemGroupProps
|
||||
>(function SelectItemGroup(props, ref) {
|
||||
const { children, label, ...rest } = props
|
||||
return (
|
||||
<ChakraSelect.ItemGroup {...rest} ref={ref}>
|
||||
<ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
|
||||
{children}
|
||||
</ChakraSelect.ItemGroup>
|
||||
)
|
||||
})
|
||||
|
||||
export const SelectLabel = ChakraSelect.Label
|
||||
export const SelectItemText = ChakraSelect.ItemText
|
||||
47
src/components/ui/skeleton.tsx
Normal file
47
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
SkeletonProps as ChakraSkeletonProps,
|
||||
CircleProps,
|
||||
} from "@chakra-ui/react"
|
||||
import { Skeleton as ChakraSkeleton, Circle, Stack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface SkeletonCircleProps extends ChakraSkeletonProps {
|
||||
size?: CircleProps["size"]
|
||||
}
|
||||
|
||||
export const SkeletonCircle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
SkeletonCircleProps
|
||||
>(function SkeletonCircle(props, ref) {
|
||||
const { size, ...rest } = props
|
||||
return (
|
||||
<Circle size={size} asChild ref={ref}>
|
||||
<ChakraSkeleton {...rest} />
|
||||
</Circle>
|
||||
)
|
||||
})
|
||||
|
||||
export interface SkeletonTextProps extends ChakraSkeletonProps {
|
||||
noOfLines?: number
|
||||
}
|
||||
|
||||
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
|
||||
function SkeletonText(props, ref) {
|
||||
const { noOfLines = 3, gap, ...rest } = props
|
||||
return (
|
||||
<Stack gap={gap} width="full" ref={ref}>
|
||||
{Array.from({ length: noOfLines }).map((_, index) => (
|
||||
<ChakraSkeleton
|
||||
height="4"
|
||||
key={index}
|
||||
{...props}
|
||||
_last={{ maxW: "80%" }}
|
||||
{...rest}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const Skeleton = ChakraSkeleton
|
||||
60
src/components/ui/slider.tsx
Normal file
60
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Slider as ChakraSlider, HStack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface SliderProps extends ChakraSlider.RootProps {
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||
label?: React.ReactNode
|
||||
showValue?: boolean
|
||||
}
|
||||
|
||||
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
||||
function Slider(props, ref) {
|
||||
const { marks: marksProp, label, showValue, ...rest } = props
|
||||
const value = props.defaultValue ?? props.value
|
||||
|
||||
const marks = marksProp?.map((mark) => {
|
||||
if (typeof mark === "number") return { value: mark, label: undefined }
|
||||
return mark
|
||||
})
|
||||
|
||||
const hasMarkLabel = !!marks?.some((mark) => mark.label)
|
||||
|
||||
return (
|
||||
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
|
||||
{label && !showValue && (
|
||||
<ChakraSlider.Label fontWeight="medium">{label}</ChakraSlider.Label>
|
||||
)}
|
||||
{label && showValue && (
|
||||
<HStack justify="space-between">
|
||||
<ChakraSlider.Label fontWeight="medium">{label}</ChakraSlider.Label>
|
||||
<ChakraSlider.ValueText />
|
||||
</HStack>
|
||||
)}
|
||||
<ChakraSlider.Control mb={hasMarkLabel ? "4" : undefined}>
|
||||
<ChakraSlider.Track>
|
||||
<ChakraSlider.Range />
|
||||
</ChakraSlider.Track>
|
||||
{value?.map((_, index) => (
|
||||
<ChakraSlider.Thumb key={index} index={index}>
|
||||
<ChakraSlider.HiddenInput />
|
||||
</ChakraSlider.Thumb>
|
||||
))}
|
||||
</ChakraSlider.Control>
|
||||
{marks?.length && (
|
||||
<ChakraSlider.MarkerGroup>
|
||||
{marks.map((mark, index) => {
|
||||
const value = typeof mark === "number" ? mark : mark.value
|
||||
const label = typeof mark === "number" ? undefined : mark.label
|
||||
return (
|
||||
<ChakraSlider.Marker key={index} value={value}>
|
||||
<ChakraSlider.MarkerIndicator />
|
||||
{label}
|
||||
</ChakraSlider.Marker>
|
||||
)
|
||||
})}
|
||||
</ChakraSlider.MarkerGroup>
|
||||
)}
|
||||
</ChakraSlider.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
68
src/components/ui/stat.tsx
Normal file
68
src/components/ui/stat.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Badge,
|
||||
type BadgeProps,
|
||||
Stat as ChakraStat,
|
||||
FormatNumber,
|
||||
} from "@chakra-ui/react"
|
||||
import { InfoTip } from "./toggle-tip"
|
||||
import * as React from "react"
|
||||
|
||||
interface StatLabelProps extends ChakraStat.LabelProps {
|
||||
info?: React.ReactNode
|
||||
}
|
||||
|
||||
export const StatLabel = React.forwardRef<HTMLDivElement, StatLabelProps>(
|
||||
function StatLabel(props, ref) {
|
||||
const { info, children, ...rest } = props
|
||||
return (
|
||||
<ChakraStat.Label {...rest} ref={ref}>
|
||||
{children}
|
||||
{info && <InfoTip>{info}</InfoTip>}
|
||||
</ChakraStat.Label>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface StatValueTextProps extends ChakraStat.ValueTextProps {
|
||||
value?: number
|
||||
formatOptions?: Intl.NumberFormatOptions
|
||||
}
|
||||
|
||||
export const StatValueText = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
StatValueTextProps
|
||||
>(function StatValueText(props, ref) {
|
||||
const { value, formatOptions, children, ...rest } = props
|
||||
return (
|
||||
<ChakraStat.ValueText {...rest} ref={ref}>
|
||||
{children ||
|
||||
(value != null && <FormatNumber value={value} {...formatOptions} />)}
|
||||
</ChakraStat.ValueText>
|
||||
)
|
||||
})
|
||||
|
||||
export const StatUpTrend = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
function StatUpTrend(props, ref) {
|
||||
return (
|
||||
<Badge colorPalette="green" gap="0" {...props} ref={ref}>
|
||||
<ChakraStat.UpIndicator />
|
||||
{props.children}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const StatDownTrend = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
function StatDownTrend(props, ref) {
|
||||
return (
|
||||
<Badge colorPalette="red" gap="0" {...props} ref={ref}>
|
||||
<ChakraStat.DownIndicator />
|
||||
{props.children}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const StatRoot = ChakraStat.Root
|
||||
export const StatHelpText = ChakraStat.HelpText
|
||||
export const StatValueUnit = ChakraStat.ValueUnit
|
||||
29
src/components/ui/status.tsx
Normal file
29
src/components/ui/status.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ColorPalette } from "@chakra-ui/react"
|
||||
import { Status as ChakraStatus } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
type StatusValue = "success" | "error" | "warning" | "info"
|
||||
|
||||
export interface StatusProps extends ChakraStatus.RootProps {
|
||||
value?: StatusValue
|
||||
}
|
||||
|
||||
const statusMap: Record<StatusValue, ColorPalette> = {
|
||||
success: "green",
|
||||
error: "red",
|
||||
warning: "orange",
|
||||
info: "blue",
|
||||
}
|
||||
|
||||
export const Status = React.forwardRef<HTMLDivElement, StatusProps>(
|
||||
function Status(props, ref) {
|
||||
const { children, value = "info", ...rest } = props
|
||||
const colorPalette = rest.colorPalette ?? statusMap[value]
|
||||
return (
|
||||
<ChakraStatus.Root ref={ref} {...rest} colorPalette={colorPalette}>
|
||||
<ChakraStatus.Indicator />
|
||||
{children}
|
||||
</ChakraStatus.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
49
src/components/ui/stepper-input.tsx
Normal file
49
src/components/ui/stepper-input.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { HStack, IconButton, NumberInput } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuMinus, LuPlus } from "react-icons/lu"
|
||||
|
||||
export interface StepperInputProps extends NumberInput.RootProps {
|
||||
label?: React.ReactNode
|
||||
}
|
||||
|
||||
export const StepperInput = React.forwardRef<HTMLDivElement, StepperInputProps>(
|
||||
function StepperInput(props, ref) {
|
||||
const { label, ...rest } = props
|
||||
return (
|
||||
<NumberInput.Root {...rest} unstyled ref={ref}>
|
||||
{label && <NumberInput.Label>{label}</NumberInput.Label>}
|
||||
<HStack gap="2">
|
||||
<DecrementTrigger />
|
||||
<NumberInput.ValueText textAlign="center" fontSize="lg" minW="3ch" />
|
||||
<IncrementTrigger />
|
||||
</HStack>
|
||||
</NumberInput.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const DecrementTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
NumberInput.DecrementTriggerProps
|
||||
>(function DecrementTrigger(props, ref) {
|
||||
return (
|
||||
<NumberInput.DecrementTrigger {...props} asChild ref={ref}>
|
||||
<IconButton variant="outline" size="sm">
|
||||
<LuMinus />
|
||||
</IconButton>
|
||||
</NumberInput.DecrementTrigger>
|
||||
)
|
||||
})
|
||||
|
||||
const IncrementTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
NumberInput.IncrementTriggerProps
|
||||
>(function IncrementTrigger(props, ref) {
|
||||
return (
|
||||
<NumberInput.IncrementTrigger {...props} asChild ref={ref}>
|
||||
<IconButton variant="outline" size="sm">
|
||||
<LuPlus />
|
||||
</IconButton>
|
||||
</NumberInput.IncrementTrigger>
|
||||
)
|
||||
})
|
||||
82
src/components/ui/steps.tsx
Normal file
82
src/components/ui/steps.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Box, Steps as ChakraSteps } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuCheck } from "react-icons/lu"
|
||||
|
||||
interface StepInfoProps {
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
}
|
||||
|
||||
export interface StepsItemProps
|
||||
extends Omit<ChakraSteps.ItemProps, "title">,
|
||||
StepInfoProps {
|
||||
completedIcon?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const StepsItem = React.forwardRef<HTMLDivElement, StepsItemProps>(
|
||||
function StepsItem(props, ref) {
|
||||
const { title, description, completedIcon, icon, ...rest } = props
|
||||
return (
|
||||
<ChakraSteps.Item {...rest} ref={ref}>
|
||||
<ChakraSteps.Trigger>
|
||||
<ChakraSteps.Indicator>
|
||||
<ChakraSteps.Status
|
||||
complete={completedIcon || <LuCheck />}
|
||||
incomplete={icon || <ChakraSteps.Number />}
|
||||
/>
|
||||
</ChakraSteps.Indicator>
|
||||
<StepInfo title={title} description={description} />
|
||||
</ChakraSteps.Trigger>
|
||||
<ChakraSteps.Separator />
|
||||
</ChakraSteps.Item>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const StepInfo = (props: StepInfoProps) => {
|
||||
const { title, description } = props
|
||||
|
||||
if (title && description) {
|
||||
return (
|
||||
<Box>
|
||||
<ChakraSteps.Title>{title}</ChakraSteps.Title>
|
||||
<ChakraSteps.Description>{description}</ChakraSteps.Description>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && <ChakraSteps.Title>{title}</ChakraSteps.Title>}
|
||||
{description && (
|
||||
<ChakraSteps.Description>{description}</ChakraSteps.Description>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface StepsIndicatorProps {
|
||||
completedIcon: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
}
|
||||
|
||||
export const StepsIndicator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
StepsIndicatorProps
|
||||
>(function StepsIndicator(props, ref) {
|
||||
const { icon = <ChakraSteps.Number />, completedIcon } = props
|
||||
return (
|
||||
<ChakraSteps.Indicator ref={ref}>
|
||||
<ChakraSteps.Status complete={completedIcon} incomplete={icon} />
|
||||
</ChakraSteps.Indicator>
|
||||
)
|
||||
})
|
||||
|
||||
export const StepsList = ChakraSteps.List
|
||||
export const StepsRoot = ChakraSteps.Root
|
||||
export const StepsContent = ChakraSteps.Content
|
||||
export const StepsCompletedContent = ChakraSteps.CompletedContent
|
||||
|
||||
export const StepsNextTrigger = ChakraSteps.NextTrigger
|
||||
export const StepsPrevTrigger = ChakraSteps.PrevTrigger
|
||||
39
src/components/ui/switch.tsx
Normal file
39
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Switch as ChakraSwitch } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface SwitchProps extends ChakraSwitch.RootProps {
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
rootRef?: React.Ref<HTMLLabelElement>
|
||||
trackLabel?: { on: React.ReactNode; off: React.ReactNode }
|
||||
thumbLabel?: { on: React.ReactNode; off: React.ReactNode }
|
||||
}
|
||||
|
||||
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
|
||||
function Switch(props, ref) {
|
||||
const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } =
|
||||
props
|
||||
|
||||
return (
|
||||
<ChakraSwitch.Root ref={rootRef} {...rest}>
|
||||
<ChakraSwitch.HiddenInput ref={ref} {...inputProps} />
|
||||
<ChakraSwitch.Control>
|
||||
<ChakraSwitch.Thumb>
|
||||
{thumbLabel && (
|
||||
<ChakraSwitch.ThumbIndicator fallback={thumbLabel?.off}>
|
||||
{thumbLabel?.on}
|
||||
</ChakraSwitch.ThumbIndicator>
|
||||
)}
|
||||
</ChakraSwitch.Thumb>
|
||||
{trackLabel && (
|
||||
<ChakraSwitch.Indicator fallback={trackLabel.off}>
|
||||
{trackLabel.on}
|
||||
</ChakraSwitch.Indicator>
|
||||
)}
|
||||
</ChakraSwitch.Control>
|
||||
{children != null && (
|
||||
<ChakraSwitch.Label>{children}</ChakraSwitch.Label>
|
||||
)}
|
||||
</ChakraSwitch.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
39
src/components/ui/tag.tsx
Normal file
39
src/components/ui/tag.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Tag as ChakraTag } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface TagProps extends ChakraTag.RootProps {
|
||||
startElement?: React.ReactNode
|
||||
endElement?: React.ReactNode
|
||||
onClose?: VoidFunction
|
||||
closable?: boolean
|
||||
}
|
||||
|
||||
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
||||
function Tag(props, ref) {
|
||||
const {
|
||||
startElement,
|
||||
endElement,
|
||||
onClose,
|
||||
closable = !!onClose,
|
||||
children,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<ChakraTag.Root ref={ref} {...rest}>
|
||||
{startElement && (
|
||||
<ChakraTag.StartElement>{startElement}</ChakraTag.StartElement>
|
||||
)}
|
||||
<ChakraTag.Label>{children}</ChakraTag.Label>
|
||||
{endElement && (
|
||||
<ChakraTag.EndElement>{endElement}</ChakraTag.EndElement>
|
||||
)}
|
||||
{closable && (
|
||||
<ChakraTag.EndElement>
|
||||
<ChakraTag.CloseTrigger onClick={onClose} />
|
||||
</ChakraTag.EndElement>
|
||||
)}
|
||||
</ChakraTag.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
21
src/components/ui/timeline.tsx
Normal file
21
src/components/ui/timeline.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Timeline as ChakraTimeline } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export const TimelineConnector = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraTimeline.IndicatorProps
|
||||
>(function TimelineConnector(props, ref) {
|
||||
return (
|
||||
<ChakraTimeline.Connector ref={ref}>
|
||||
<ChakraTimeline.Separator />
|
||||
<ChakraTimeline.Indicator {...props} />
|
||||
</ChakraTimeline.Connector>
|
||||
)
|
||||
})
|
||||
|
||||
export const TimelineRoot = ChakraTimeline.Root
|
||||
export const TimelineContent = ChakraTimeline.Content
|
||||
export const TimelineItem = ChakraTimeline.Item
|
||||
export const TimelineIndicator = ChakraTimeline.Indicator
|
||||
export const TimelineTitle = ChakraTimeline.Title
|
||||
export const TimelineDescription = ChakraTimeline.Description
|
||||
43
src/components/ui/toaster.tsx
Normal file
43
src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Toaster as ChakraToaster,
|
||||
Portal,
|
||||
Spinner,
|
||||
Stack,
|
||||
Toast,
|
||||
createToaster,
|
||||
} from "@chakra-ui/react"
|
||||
|
||||
export const toaster = createToaster({
|
||||
placement: "bottom-end",
|
||||
pauseOnPageIdle: true,
|
||||
})
|
||||
|
||||
export const Toaster = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||
{(toast) => (
|
||||
<Toast.Root width={{ md: "sm" }}>
|
||||
{toast.type === "loading" ? (
|
||||
<Spinner size="sm" color="blue.solid" />
|
||||
) : (
|
||||
<Toast.Indicator />
|
||||
)}
|
||||
<Stack gap="1" flex="1" maxWidth="100%">
|
||||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||
{toast.description && (
|
||||
<Toast.Description>{toast.description}</Toast.Description>
|
||||
)}
|
||||
</Stack>
|
||||
{toast.action && (
|
||||
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||
)}
|
||||
{toast.meta?.closable && <Toast.CloseTrigger />}
|
||||
</Toast.Root>
|
||||
)}
|
||||
</ChakraToaster>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
70
src/components/ui/toggle-tip.tsx
Normal file
70
src/components/ui/toggle-tip.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Popover as ChakraPopover, IconButton, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { HiOutlineInformationCircle } from "react-icons/hi"
|
||||
|
||||
export interface ToggleTipProps extends ChakraPopover.RootProps {
|
||||
showArrow?: boolean
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
content?: React.ReactNode
|
||||
}
|
||||
|
||||
export const ToggleTip = React.forwardRef<HTMLDivElement, ToggleTipProps>(
|
||||
function ToggleTip(props, ref) {
|
||||
const {
|
||||
showArrow,
|
||||
children,
|
||||
portalled = true,
|
||||
content,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<ChakraPopover.Root
|
||||
{...rest}
|
||||
positioning={{ ...rest.positioning, gutter: 4 }}
|
||||
>
|
||||
<ChakraPopover.Trigger asChild>{children}</ChakraPopover.Trigger>
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraPopover.Positioner>
|
||||
<ChakraPopover.Content
|
||||
width="auto"
|
||||
px="2"
|
||||
py="1"
|
||||
textStyle="xs"
|
||||
rounded="sm"
|
||||
ref={ref}
|
||||
>
|
||||
{showArrow && (
|
||||
<ChakraPopover.Arrow>
|
||||
<ChakraPopover.ArrowTip />
|
||||
</ChakraPopover.Arrow>
|
||||
)}
|
||||
{content}
|
||||
</ChakraPopover.Content>
|
||||
</ChakraPopover.Positioner>
|
||||
</Portal>
|
||||
</ChakraPopover.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const InfoTip = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
Partial<ToggleTipProps>
|
||||
>(function InfoTip(props, ref) {
|
||||
const { children, ...rest } = props
|
||||
return (
|
||||
<ToggleTip content={children} {...rest} ref={ref}>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
aria-label="info"
|
||||
size="2xs"
|
||||
colorPalette="gray"
|
||||
>
|
||||
<HiOutlineInformationCircle />
|
||||
</IconButton>
|
||||
</ToggleTip>
|
||||
)
|
||||
})
|
||||
57
src/components/ui/toggle.tsx
Normal file
57
src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import type { ButtonProps } from "@chakra-ui/react"
|
||||
import {
|
||||
Button,
|
||||
Toggle as ChakraToggle,
|
||||
useToggleContext,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
interface ToggleProps extends ChakraToggle.RootProps {
|
||||
variant?: keyof typeof variantMap
|
||||
size?: ButtonProps["size"]
|
||||
}
|
||||
|
||||
const variantMap = {
|
||||
solid: { on: "solid", off: "outline" },
|
||||
surface: { on: "surface", off: "outline" },
|
||||
subtle: { on: "subtle", off: "ghost" },
|
||||
ghost: { on: "subtle", off: "ghost" },
|
||||
} as const
|
||||
|
||||
export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
||||
function Toggle(props, ref) {
|
||||
const { variant = "subtle", size, children, ...rest } = props
|
||||
const variantConfig = variantMap[variant]
|
||||
|
||||
return (
|
||||
<ChakraToggle.Root asChild {...rest}>
|
||||
<ToggleBaseButton size={size} variant={variantConfig} ref={ref}>
|
||||
{children}
|
||||
</ToggleBaseButton>
|
||||
</ChakraToggle.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
interface ToggleBaseButtonProps extends Omit<ButtonProps, "variant"> {
|
||||
variant: Record<"on" | "off", ButtonProps["variant"]>
|
||||
}
|
||||
|
||||
const ToggleBaseButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ToggleBaseButtonProps
|
||||
>(function ToggleBaseButton(props, ref) {
|
||||
const toggle = useToggleContext()
|
||||
const { variant, ...rest } = props
|
||||
return (
|
||||
<Button
|
||||
variant={toggle.pressed ? variant.on : variant.off}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export const ToggleIndicator = ChakraToggle.Indicator
|
||||
46
src/components/ui/tooltip.tsx
Normal file
46
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||
showArrow?: boolean
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
content: React.ReactNode
|
||||
contentProps?: ChakraTooltip.ContentProps
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
function Tooltip(props, ref) {
|
||||
const {
|
||||
showArrow,
|
||||
children,
|
||||
disabled,
|
||||
portalled,
|
||||
content,
|
||||
contentProps,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (disabled) return children
|
||||
|
||||
return (
|
||||
<ChakraTooltip.Root {...rest}>
|
||||
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraTooltip.Positioner>
|
||||
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||
{showArrow && (
|
||||
<ChakraTooltip.Arrow>
|
||||
<ChakraTooltip.ArrowTip />
|
||||
</ChakraTooltip.Arrow>
|
||||
)}
|
||||
{content}
|
||||
</ChakraTooltip.Content>
|
||||
</ChakraTooltip.Positioner>
|
||||
</Portal>
|
||||
</ChakraTooltip.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user