moved stuff out of app since it is used for routing

This commit is contained in:
2024-11-20 23:16:26 -05:00
parent d1bd68e720
commit fd08fd2886
136 changed files with 56 additions and 46 deletions

View 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>
)
)
}

View File

@@ -0,0 +1,10 @@
import Link from "next/link";
export default function BB_Base_Component() {
return (
(
<div />
)
)
}

View 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? Wed 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>
)
)
}

View 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>
)
)
}

View 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>&copy; {new Date().getFullYear()} Firearm Builder. All rights reserved.</p>
</div>
</footer>
)
)
}

View 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>)
);
}

View 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;

View 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/"
}
]
}

View File

@@ -0,0 +1,7 @@
.navLinks {
color:#000;
}
.navLinks:hover {
font-weight: bold;
text-decoration : none;
}

View 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;

View 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/"
}
]
}

View 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;
}
}

View 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 = {
};

View 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/"
}
]
}

View 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;
}

View 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>
);
}
}

View 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/"
}
]
}

View File

View 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 = {
};

View 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/"
}
]
}

View 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 = {
};

View 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/"
}
]
}

View 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">&copy;&nbsp; {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;
}
}
`

View 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/"
}
]
}

View File

@@ -0,0 +1,7 @@
.white {
color: white;
font-size: .5em;
text-transform: uppercase;
letter-spacing: 2px;
font-family: "hind";
}

View 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 = {
};

View 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/"
}
]
}

View 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>
)
}

View 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/"
}
]
}

View 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 = {
};

View 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/"
}
]
}

View 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>
)
}

View 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/"
}
]
}

View 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 = {
};

View 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/"
}
]
}

View 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>
);
}

View 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/"
}
]
}

View File

@@ -0,0 +1 @@

View 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;

View 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>
)
)}

View 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>
)
}

View 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 />
</>
)
)
}

View 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

View 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

View 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>
)
},
)

View 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>
)
},
)

View 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 ? <>&mdash;</> : null} <cite>{cite}</cite>
</ChakraBlockquote.Caption>
)}
</ChakraBlockquote.Root>
)
},
)
export const BlockquoteIcon = ChakraBlockquote.Icon

View 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

View 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>
)
},
)

View 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

View 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>
)
},
)

View 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

View 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>
)
})

View 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>
)
})

View 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>
)
},
)

View 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

View 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

View 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>
)
},
)

View 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>
)
},
)

View 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

View 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

View 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>
)
},
)

View 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
View 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

View 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>
)
})

View 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

View 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>
)
})

View 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" }
}
}

View 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>
)
},
)

View 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

View 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

View 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
View 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",
},
})

View 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>
)
}

View 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

View 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

View 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>
)
},
)

View 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>
)
})

View 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

View 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

View 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>
)
},
)

View 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

View 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>
)
},
)

View 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>
)
})

View 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

View 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
View 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>
)
},
)

View 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

View 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>
)
}

View 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>
)
})

View 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

View 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>
)
},
)