├── .github
└── FUNDING.yml
├── project
├── client
│ ├── .travis.yml
│ ├── src
│ │ ├── .eslintrc
│ │ ├── components
│ │ │ ├── Navigation
│ │ │ │ ├── index.js
│ │ │ │ └── NavbarOne.jsx
│ │ │ ├── Footer
│ │ │ │ ├── index.js
│ │ │ │ └── FooterOne.jsx
│ │ │ ├── Hero
│ │ │ │ ├── Hero.jsx
│ │ │ │ └── HeroOne.jsx
│ │ │ ├── CallToAction
│ │ │ │ └── CallToActionOne.jsx
│ │ │ ├── PasswordReset
│ │ │ │ └── Password.jsx
│ │ │ ├── Pricing
│ │ │ │ ├── PricingSectionOne.jsx
│ │ │ │ └── PlanCardOne.jsx
│ │ │ ├── Benefits
│ │ │ │ └── BenefitsOne.jsx
│ │ │ ├── SignInForm
│ │ │ │ └── SignInFormOne.jsx
│ │ │ └── SignUpForm
│ │ │ │ └── SignUpFormOne.jsx
│ │ ├── constants
│ │ │ ├── urls.js
│ │ │ └── routes.js
│ │ ├── lib
│ │ │ ├── Auth
│ │ │ │ ├── index.js
│ │ │ │ ├── auth.js
│ │ │ │ └── context.jsx
│ │ │ ├── Firebase
│ │ │ │ ├── index.js
│ │ │ │ ├── context.jsx
│ │ │ │ └── firebase.js
│ │ │ ├── withLoadingSpinner.jsx
│ │ │ ├── withStaq.jsx
│ │ │ ├── StaqStyleProvider.jsx
│ │ │ ├── withStripe.jsx
│ │ │ ├── signup.js
│ │ │ └── StaqRoutes.jsx
│ │ ├── styles.module.css
│ │ ├── pages
│ │ │ ├── SignUpPage
│ │ │ │ ├── SignUpPageOne.jsx
│ │ │ │ └── SignUpPage.jsx
│ │ │ ├── SignInPage
│ │ │ │ ├── SignInPageOne.jsx
│ │ │ │ └── SignInPage.jsx
│ │ │ ├── PricingPage
│ │ │ │ ├── PricingPageOne.jsx
│ │ │ │ └── PricingPage.jsx
│ │ │ ├── ForgotPasswordPage
│ │ │ │ ├── ForgotPasswordPage.jsx
│ │ │ │ └── ForgotPasswordPageOne.jsx
│ │ │ └── LandingPage
│ │ │ │ ├── LandingPage.js
│ │ │ │ ├── LandingPageOne.jsx
│ │ │ │ └── LandingPageTwo.jsx
│ │ └── index.js
│ ├── postcss.config.js
│ ├── styles
│ │ └── tailwind.css
│ ├── landing-page.png
│ ├── babel.config.json
│ ├── .editorconfig
│ ├── jsconfig.json
│ ├── .prettierrc
│ ├── tailwind.config.js
│ ├── .eslintrc.json
│ ├── rollup.config.js
│ ├── package.json
│ └── tailwind.js
├── server
│ ├── util.js
│ ├── index.js
│ ├── functions
│ │ ├── createStripeCustomerPortalSession.js
│ │ ├── createStripeCheckoutSession.js
│ │ ├── onStripeCheckoutSessionCompleted.js
│ │ └── stripeCustomer.js
│ └── package.json
└── staq.js
├── jsconfig.json
├── .gitignore
├── LICENSE
├── CODE_OF_CONDUCT.md
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: mroll
2 |
3 |
4 |
--------------------------------------------------------------------------------
/project/client/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12
4 | - 10
5 |
--------------------------------------------------------------------------------
/project/client/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/project/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('tailwindcss')],
3 | }
4 |
--------------------------------------------------------------------------------
/project/client/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/project/client/landing-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/staqjs/staq/HEAD/project/client/landing-page.png
--------------------------------------------------------------------------------
/project/client/src/components/Navigation/index.js:
--------------------------------------------------------------------------------
1 | import NavbarOne from './NavbarOne'
2 |
3 | export { NavbarOne }
4 |
--------------------------------------------------------------------------------
/project/client/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import FooterOne from './FooterOne'
2 |
3 | export {
4 | FooterOne
5 | }
6 |
--------------------------------------------------------------------------------
/project/client/src/constants/urls.js:
--------------------------------------------------------------------------------
1 | export const STRIPE_CUSTOMER = '/customer'
2 | export const STRIPE_SUBSCRIPTION = '/subscription'
3 |
--------------------------------------------------------------------------------
/project/client/src/lib/Auth/index.js:
--------------------------------------------------------------------------------
1 | import AuthProvider, { withAuth } from './context'
2 |
3 | export {
4 | AuthProvider,
5 | withAuth,
6 | }
7 |
--------------------------------------------------------------------------------
/project/client/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-react", "@babel/preset-env"],
3 | "plugins": ["@babel/plugin-transform-react-jsx", "@babel/plugin-proposal-class-properties"]
4 | }
5 |
--------------------------------------------------------------------------------
/project/client/src/lib/Firebase/index.js:
--------------------------------------------------------------------------------
1 | import FirebaseContext, { withFirebase } from './context'
2 | import Firebase from './firebase'
3 |
4 | export default Firebase
5 | export { FirebaseContext, withFirebase }
6 |
--------------------------------------------------------------------------------
/project/client/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/project/client/src/styles.module.css:
--------------------------------------------------------------------------------
1 | /* add css module styles here (optional) */
2 |
3 | .test {
4 | margin: 2em;
5 | padding: 0.5em;
6 | border: 2px solid #000;
7 | font-size: 2em;
8 | text-align: center;
9 | }
10 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "allowSyntheticDefaultImports": true,
5 | "noEmit": true,
6 | "checkJs": true,
7 | "jsx": "react",
8 | "lib": [ "dom", "es2017" ]
9 | }
10 | }
--------------------------------------------------------------------------------
/project/client/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "allowSyntheticDefaultImports": true,
5 | "noEmit": true,
6 | "checkJs": true,
7 | "jsx": "react",
8 | "lib": [ "dom", "es2017" ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/project/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": false,
4 | "semi": false,
5 | "tabWidth": 2,
6 | "bracketSpacing": true,
7 | "jsxBracketSameLine": false,
8 | "arrowParens": "always",
9 | "trailingComma": "all"
10 | }
11 |
--------------------------------------------------------------------------------
/project/client/src/constants/routes.js:
--------------------------------------------------------------------------------
1 | export const Landing = '/'
2 | export const SignUp = '/signup'
3 | export const SignIn = '/signin'
4 | export const Pricing = '/pricing'
5 | export const Demo = '/demo'
6 | export const UserSettings = '/settings/user'
7 | export const BillingSettings = '/settings/billing'
8 | export const ForgotPassword = '/forgot-password'
9 |
--------------------------------------------------------------------------------
/project/client/src/lib/Firebase/context.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const FirebaseContext = React.createContext(null)
4 |
5 | export const withFirebase = (Component) => (props) => (
6 |
7 | {(firebase) => }
8 |
9 | )
10 |
11 | export default FirebaseContext
12 |
--------------------------------------------------------------------------------
/project/client/src/lib/withLoadingSpinner.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { CgSpinner } from 'react-icons/cg'
3 |
4 | export function withLoadingSpinner(component, isLoading) {
5 | return function () {
6 | return isLoading ? (
7 |
8 | ) : (
9 | component
10 | )
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | .log
--------------------------------------------------------------------------------
/project/client/src/components/Hero/Hero.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import HeroOne from './HeroOne'
3 |
4 | const getHeroComponent = (templateName) => {
5 | if (templateName === 'One') {
6 | return HeroOne
7 | }
8 |
9 | return HeroOne
10 | }
11 |
12 | function Hero(props) {
13 | const { templateName } = props
14 | const HeroComponent = getHeroComponent(templateName)
15 |
16 | return
17 | }
18 |
19 | export default Hero
20 |
--------------------------------------------------------------------------------
/project/client/src/pages/SignUpPage/SignUpPageOne.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useHistory } from 'react-router-dom'
3 |
4 | import { signup } from '../../lib/signup'
5 |
6 | import SignUpFormOne from '../../components/SignUpForm/SignUpFormOne'
7 |
8 | function SignUpPageOne(props) {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default SignUpPageOne
17 |
--------------------------------------------------------------------------------
/project/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 |
3 | module.exports = {
4 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
5 | plugins: [],
6 | prefix: 'sjs-',
7 | theme: {
8 | extend: {
9 | colors: {
10 | primary: colors.violet[600],
11 | },
12 | maxWidth: {
13 | '1/4': '25%',
14 | '1/2': '50%',
15 | '3/4': '75%',
16 | '2/5': '40%',
17 | '3/5': '60%',
18 | },
19 | },
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/project/server/util.js:
--------------------------------------------------------------------------------
1 | import staqConfig from '../staq'
2 | const { SecretManagerServiceClient } = require('@google-cloud/secret-manager')
3 |
4 | export const getSecret = async (secretName) => {
5 | const projectNumber = staqConfig.get('gcpProjectNumber')
6 | const secretPath = `projects/${projectNumber}/secrets/${secretName}/versions/latest`
7 | const smClient = new SecretManagerServiceClient()
8 | const [version] = await smClient.accessSecretVersion({
9 | name: secretPath
10 | })
11 | return version.payload.data.toString()
12 | }
13 |
--------------------------------------------------------------------------------
/project/server/index.js:
--------------------------------------------------------------------------------
1 | import stripeCustomer from './functions/stripeCustomer'
2 | import createStripeCustomerPortalSession from './functions/createStripeCustomerPortalSession'
3 | import createStripeCheckoutSession from './functions/createStripeCheckoutSession'
4 | import onStripeCheckoutSessionCompleted from './functions/onStripeCheckoutSessionCompleted'
5 | import { initStaq } from '../staq'
6 | import { getSecret } from './util'
7 |
8 | export {
9 | getSecret,
10 | initStaq,
11 | createStripeCheckoutSession,
12 | createStripeCustomerPortalSession,
13 | onStripeCheckoutSessionCompleted,
14 | stripeCustomer,
15 | }
16 |
--------------------------------------------------------------------------------
/project/client/src/lib/withStaq.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Firebase, { FirebaseContext } from './Firebase'
3 | import { AuthProvider } from './Auth'
4 | import StripeProvider from './withStripe'
5 | import staqConfig from '../../../staq'
6 |
7 | export default (children) => {
8 | const firebase = new Firebase(staqConfig.get('FirebaseConfig'))
9 | staqConfig.set('firebase', firebase)
10 |
11 | return (
12 |
13 |
14 | {children}
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/project/client/src/pages/SignInPage/SignInPageOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect } from 'react-router-dom'
3 |
4 | import staqConfig from '../../../../staq'
5 | import { withAuth } from '../../lib/Auth'
6 |
7 | import SignInFormOne from '../../components/SignInForm/SignInFormOne'
8 |
9 | function SignInPageOne(props) {
10 | const { auth } = props
11 | const userHome = staqConfig.get('userHome') || '/'
12 |
13 | return auth.currentUser ? (
14 |
15 | ) : (
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default withAuth(SignInPageOne)
23 |
--------------------------------------------------------------------------------
/project/server/functions/createStripeCustomerPortalSession.js:
--------------------------------------------------------------------------------
1 | import { getSecret } from '../util'
2 |
3 | const functions = require('firebase-functions')
4 | const _stripe = require("stripe")
5 |
6 |
7 | async function createPortalSession(data, context, stripe) {
8 | const session = await stripe.billingPortal.sessions.create({
9 | customer: data.customerId,
10 | return_url: data.return_url
11 | })
12 |
13 | return session
14 | }
15 |
16 |
17 | export default functions.https.onCall(async (data, context) => {
18 | console.log('data', data)
19 |
20 | const stripeSecretKey = await getSecret('stripe-secret-key')
21 | const stripe = _stripe(stripeSecretKey)
22 |
23 | return createPortalSession(data, context, stripe)
24 | })
25 |
--------------------------------------------------------------------------------
/project/client/src/pages/PricingPage/PricingPageOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import staqConfig from '../../../../staq'
4 | import * as ROUTES from '../../constants/routes'
5 |
6 | import PricingSectionOne from '../../components/Pricing/PricingSectionOne'
7 | import CallToActionOne from '../../components/CallToAction/CallToActionOne'
8 |
9 | function PricingPageOne() {
10 | const pricingProps = staqConfig.get('Template.Config.Pricing', {})
11 | const callToActionProps = staqConfig.get('Template.Config.CallToAction', {})
12 |
13 | return (
14 |
18 | )
19 | }
20 |
21 | export default PricingPageOne
22 |
--------------------------------------------------------------------------------
/project/client/src/components/CallToAction/CallToActionOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | function CallToActionOne(props) {
5 | const { Title, ActionText, ActionLink } = props
6 |
7 | return (
8 |
9 |
{Title}
10 |
11 |
12 |
18 | {ActionText}
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default CallToActionOne
26 |
--------------------------------------------------------------------------------
/project/client/src/pages/SignInPage/SignInPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect } from 'react-router-dom'
3 |
4 | import staqConfig from '../../../../staq'
5 | import { withAuth } from '../../lib/Auth'
6 |
7 | import SignInPageOne from './SignInPageOne'
8 |
9 | const getSignInPageComponent = () => {
10 | const layoutName = staqConfig.get('Template')
11 | if (layoutName === 'One') {
12 | return SignInPageOne
13 | }
14 |
15 | return SignInPageOne
16 | }
17 |
18 | function SignInPage(props) {
19 | const { auth } = props
20 | const SignInPageComponent = getSignInPageComponent()
21 |
22 | return auth.currentUser ? (
23 |
24 | ) : (
25 |
26 | )
27 | }
28 |
29 | export default withAuth(SignInPage)
30 |
--------------------------------------------------------------------------------
/project/client/src/pages/SignUpPage/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect } from 'react-router-dom'
3 |
4 | import staqConfig from '../../../../staq'
5 | import { withAuth } from '../../lib/Auth'
6 |
7 | import SignUpPageOne from './SignUpPageOne'
8 |
9 | const getSignUpPageComponent = () => {
10 | const layoutName = staqConfig.get('Template')
11 | if (layoutName === 'One') {
12 | return SignUpPageOne
13 | }
14 |
15 | return SignUpPageOne
16 | }
17 |
18 | function SignUpPage(props) {
19 | const { auth } = props
20 | const SignUpPageComponent = getSignUpPageComponent()
21 |
22 | return auth.currentUser ? (
23 |
24 | ) : (
25 |
26 | )
27 | }
28 |
29 | export default withAuth(SignUpPage)
30 |
--------------------------------------------------------------------------------
/project/client/src/pages/PricingPage/PricingPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect } from 'react-router-dom'
3 |
4 | import staqConfig from '../../../../staq'
5 | import { withAuth } from '../../lib/Auth'
6 |
7 | import PricingPageOne from './PricingPageOne'
8 |
9 | const getPricingPageComponent = () => {
10 | const layoutName = staqConfig.get('Template')
11 | if (layoutName === 'One') {
12 | return PricingPageOne
13 | }
14 |
15 | return PricingPageOne
16 | }
17 |
18 | function PricingPage(props) {
19 | const { auth } = props
20 | const PricingPageComponent = getPricingPageComponent()
21 |
22 | return auth.currentUser ? (
23 |
24 | ) : (
25 |
26 | )
27 | }
28 |
29 | export default withAuth(PricingPage)
30 |
--------------------------------------------------------------------------------
/project/client/src/components/PasswordReset/Password.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | function Password(props) {
4 | const { sendPasswordResetEmail, passwordResetMessage } = props
5 |
6 | const handleSubmit = (event) => {
7 | event.preventDefault()
8 | sendPasswordResetEmail()
9 | }
10 |
11 | return (
12 |
13 |
Password Reset
14 |
25 |
26 | )
27 | }
28 |
29 | export default Password
30 |
--------------------------------------------------------------------------------
/project/client/src/lib/StaqStyleProvider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | StylesProvider,
4 | createGenerateClassName,
5 | createMuiTheme
6 | } from '@material-ui/core/styles'
7 | import { ThemeProvider } from '@material-ui/styles'
8 |
9 | import staqConfig from '../../../staq'
10 |
11 | const generateClassName = createGenerateClassName({
12 | disableGlobal: true,
13 | productionPrefix: 'staq',
14 | seed: 'staq'
15 | })
16 |
17 | const defaultTheme = createMuiTheme()
18 |
19 | export default function StaqStyleProvider(props) {
20 | const { children } = props
21 | const theme = staqConfig.get('Theme') || defaultTheme
22 |
23 | return (
24 |
25 |
26 | {children}
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/project/client/src/pages/ForgotPasswordPage/ForgotPasswordPage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect } from 'react-router-dom'
3 |
4 | import staqConfig from '../../../../staq'
5 | import { withAuth } from '../../lib/Auth'
6 |
7 | import ForgotPasswordPageOne from './ForgotPasswordPageOne'
8 |
9 | const getForgotPasswordPageComponent = () => {
10 | const layoutName = staqConfig.get('Template')
11 | if (layoutName === 'One') {
12 | return ForgotPasswordPageOne
13 | }
14 |
15 | return ForgotPasswordPageOne
16 | }
17 |
18 | function ForgotPasswordPage(props) {
19 | const { auth } = props
20 | const ForgotPasswordPageComponent = getForgotPasswordPageComponent()
21 |
22 | return auth.currentUser ? (
23 |
24 | ) : (
25 |
26 | )
27 | }
28 |
29 | export default withAuth(ForgotPasswordPage)
30 |
--------------------------------------------------------------------------------
/project/client/src/pages/LandingPage/LandingPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Redirect } from 'react-router-dom'
4 |
5 | import staqConfig from '../../../../staq'
6 | import { withAuth } from '../../lib/Auth'
7 |
8 | import LandingPageOne from './LandingPageOne'
9 | import LandingPageTwo from './LandingPageTwo'
10 |
11 | const getLandingPageComponent = () => {
12 | const layoutName = staqConfig.get('Template')
13 | if (layoutName === 'One') {
14 | return LandingPageOne
15 | }
16 |
17 | if (layoutName === 'Two') {
18 | return LandingPageTwo
19 | }
20 |
21 | return LandingPageOne
22 | }
23 |
24 | function LandingPage(props) {
25 | const { auth } = props
26 | const LandingPageComponent = getLandingPageComponent()
27 |
28 | return auth.currentUser ? (
29 |
30 | ) : (
31 |
32 | )
33 | }
34 |
35 | export default withAuth(LandingPage)
36 |
--------------------------------------------------------------------------------
/project/client/src/index.js:
--------------------------------------------------------------------------------
1 | import { initStaq } from '../../staq'
2 |
3 | // Components
4 | // NOTE: Not exporting components for now.
5 | // Export pages built from components.
6 | // Will export components in the future.
7 | // ...
8 |
9 | // Pages
10 | import LandingPage from './pages/LandingPage/LandingPage'
11 |
12 | // Components
13 | import Hero from './components/Hero/Hero'
14 |
15 | // Lib
16 | import withStaq from './lib/withStaq'
17 | import StaqRoutes, { PrivateRoute } from './lib/StaqRoutes'
18 | import { getStripeCheckoutSession, withStripe } from './lib/withStripe'
19 | import { withFirebase } from './lib/Firebase'
20 | import { withLoadingSpinner } from './lib/withLoadingSpinner'
21 | import { withAuth } from './lib/Auth'
22 |
23 | export {
24 | PrivateRoute,
25 | StaqRoutes,
26 | LandingPage,
27 | Hero,
28 | initStaq,
29 | withAuth,
30 | withFirebase,
31 | withLoadingSpinner,
32 | withStaq,
33 | withStripe,
34 | getStripeCheckoutSession,
35 | }
36 |
--------------------------------------------------------------------------------
/project/server/functions/createStripeCheckoutSession.js:
--------------------------------------------------------------------------------
1 | import { getSecret } from '../util'
2 |
3 | const functions = require('firebase-functions')
4 | const _stripe = require("stripe")
5 |
6 |
7 | async function createCheckoutSession(data, context, stripe) {
8 | const session = await stripe.checkout.sessions.create({
9 | payment_method_types: ["card"],
10 | mode: "payment",
11 | customer: data.customerId,
12 | client_reference_id: data.clientReferenceId,
13 | line_items: [
14 | {
15 | price: data.priceId,
16 | quantity: 1
17 | }
18 | ],
19 | success_url: data.successUrl,
20 | cancel_url: data.cancelUrl,
21 | })
22 |
23 | return session
24 | }
25 |
26 |
27 | export default functions.https.onCall(async (data, context) => {
28 | console.log('data', data)
29 |
30 | const stripeSecretKey = await getSecret('stripe-secret-key')
31 | const stripe = _stripe(stripeSecretKey)
32 |
33 | return createCheckoutSession(data, context, stripe)
34 | })
35 |
--------------------------------------------------------------------------------
/project/client/src/pages/LandingPage/LandingPageOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import staqConfig from '../../../../staq'
4 |
5 | import HeroOne from '../../components/Hero/HeroOne'
6 | import BenefitsOne from '../../components/Benefits/BenefitsOne'
7 | import PricingSectionOne from '../../components/Pricing/PricingSectionOne'
8 | import CallToActionOne from '../../components/CallToAction/CallToActionOne'
9 |
10 | function LandingPageOne() {
11 | const heroProps = staqConfig.get('Template.Config.Hero', {})
12 | const benefitsProps = staqConfig.get('Template.Config.Benefits', [])
13 | const pricingProps = staqConfig.get('Template.Config.Pricing', {})
14 | const callToActionProps = staqConfig.get('Template.Config.CallToAction', {})
15 |
16 | return (
17 |
23 | )
24 | }
25 |
26 | export default LandingPageOne
27 |
--------------------------------------------------------------------------------
/project/staq.js:
--------------------------------------------------------------------------------
1 | // Define this so we don't need to use lodash
2 | // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_get
3 | const _get = (obj, path, defaultValue = undefined) => {
4 | const travel = (regexp) =>
5 | String.prototype.split
6 | .call(path, regexp)
7 | .filter(Boolean)
8 | .reduce(
9 | (res, key) => (res !== null && res !== undefined ? res[key] : res),
10 | obj
11 | );
12 | const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/);
13 | return result === undefined || result === obj ? defaultValue : result;
14 | };
15 |
16 | class StaqConfig {
17 | constructor() {
18 | this.config = {};
19 | }
20 |
21 | setConfig(config) {
22 | this.config = config;
23 | }
24 |
25 | get(field, dflt) {
26 | return _get(this.config, field, dflt);
27 | }
28 |
29 | set(field, value) {
30 | this.config[field] = value;
31 | }
32 | }
33 |
34 | const staqConfig = new StaqConfig();
35 |
36 | export const initStaq = (config) => {
37 | staqConfig.config = config;
38 | };
39 |
40 | export default staqConfig;
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Matt Roll
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/project/client/src/pages/LandingPage/LandingPageTwo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import 'tailwindcss/tailwind.css'
3 |
4 | import staqConfig from '../../../../staq'
5 |
6 | import HeroOne from '../../components/Hero/HeroOne'
7 | import BenefitsOne from '../../components/Benefits/BenefitsOne'
8 | import PricingSectionOne from '../../components/Pricing/PricingSectionOne'
9 | import CallToActionOne from '../../components/CallToAction/CallToActionOne'
10 |
11 | function LandingPageOne() {
12 | const classes = useStyles()
13 |
14 | const heroProps = staqConfig.get('Template.Config.Hero', {})
15 | const benefitsProps = staqConfig.get('Template.Config.Benefits', {})
16 | const pricingProps = staqConfig.get('Template.Config.Pricing', {})
17 | const callToActionProps = staqConfig.get('Template.Config.CallToAction', {})
18 |
19 | return (
20 |
26 | )
27 | }
28 |
29 | export default LandingPageOne
30 |
--------------------------------------------------------------------------------
/project/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@staqjs/server",
3 | "version": "0.0.8",
4 | "description": "StaqJS is a Javascript library for building Software-as-a-Service (SaaS) businesses.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.modern.js",
7 | "source": "index.js",
8 | "engines": {
9 | "node": ">=10"
10 | },
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "build": "microbundle-crl --no-compress",
16 | "start": "microbundle-crl watch --no-compress --format modern,cjs",
17 | "test": "echo \"Error: no test specified\" && exit 1"
18 | },
19 | "dependencies": {
20 | "@google-cloud/secret-manager": "^3.1.0",
21 | "firebase-admin": "^9.0.0",
22 | "firebase-functions": "^3.8.0",
23 | "microbundle-crl": "^0.13.11",
24 | "stripe": "^8.81.0"
25 | },
26 | "devDependencies": {},
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/staqjs/staq.git"
30 | },
31 | "author": "Matt Roll",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/staqjs/staq/issues"
35 | },
36 | "homepage": "https://github.com/staqjs/staq#readme"
37 | }
38 |
--------------------------------------------------------------------------------
/project/client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "plugin:react/recommended",
8 | "airbnb"
9 | ],
10 | "parser": "babel-eslint",
11 | "parserOptions": {
12 | "ecmaFeatures": {
13 | "jsx": true
14 | },
15 | "ecmaVersion": 11,
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "react"
20 | ],
21 | "rules": {
22 | "semi": [
23 | 2,
24 | "never"
25 | ],
26 | "no-console": [
27 | 2,
28 | {
29 | "allow": [
30 | "error",
31 | "warn"
32 | ]
33 | }
34 | ],
35 | "no-underscore-dangle": [
36 | 2,
37 | {
38 | "allowAfterThis": true
39 | }
40 | ],
41 | "react/jsx-props-no-spreading": [
42 | 2,
43 | {
44 | "custom": "ignore"
45 | }
46 | ],
47 | "react/prop-types": 0,
48 | "arrow-body-style": 0,
49 | "no-nested-ternary": 0,
50 | "no-multi-assign": 0
51 | },
52 | "settings": {
53 | "import/resolver": {
54 | "node": {
55 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/project/client/src/components/Pricing/PricingSectionOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import PlanCardOne from './PlanCardOne'
4 |
5 | function PricingSectionOne(props) {
6 | const { Title, Subtitle, Plans } = props
7 |
8 | return (
9 |
10 |
13 |
18 | {Title}
19 |
20 |
21 |
24 |
25 |
26 | {Plans.map((plan) => (
27 |
35 | ))}
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | export default PricingSectionOne
43 |
--------------------------------------------------------------------------------
/project/client/src/components/Benefits/BenefitsOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | function BenefitsBox(props) {
4 | const { MiniTitle, Title, Message } = props
5 |
6 | return (
7 |
8 |
{MiniTitle}
9 |
{Title}
10 |
{Message}
11 |
12 | )
13 | }
14 |
15 | function BenefitsOne(props) {
16 | const { Benefits } = props
17 |
18 | return (
19 |
20 |
21 |
22 |
27 | {Benefits.map((benefit) => (
28 |
34 | ))}
35 |
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | export default BenefitsOne
43 |
--------------------------------------------------------------------------------
/project/client/src/lib/Auth/auth.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 |
3 | class Auth {
4 | constructor(firebase) {
5 | this.firebase = firebase
6 |
7 | this.currentUser = JSON.parse(localStorage.getItem('currentUser'))
8 | this.onLogoutCallback = () => {}
9 |
10 | this.initListener()
11 | }
12 |
13 | initListener() {
14 | this.listener = this.firebase.onAuthUserListener(
15 | (currentUser) => {
16 | localStorage.setItem('currentUser', JSON.stringify(currentUser))
17 | this.currentUser = currentUser
18 | },
19 | () => {
20 | localStorage.removeItem('currentUser')
21 | this.currentUser = null
22 | this.onLogoutCallback()
23 | }
24 | )
25 | }
26 |
27 | onLogout = (newCallbackOnLogout) => {
28 | this.onLogoutCallback = newCallbackOnLogout
29 | }
30 |
31 | reload = () => {
32 | this.currentUser = JSON.parse(localStorage.getItem('currentUser'))
33 | }
34 |
35 | update = (newFields) => {
36 | this
37 | .firebase.user(this.currentUser.uid)
38 | .update(newFields)
39 | .then(() => {
40 | const currentUser = _.merge({}, this.currentUser, newFields)
41 | localStorage.setItem('currentUser', JSON.stringify(currentUser))
42 | this.currentUser = currentUser
43 | })
44 | }
45 | }
46 |
47 | export default Auth
48 |
--------------------------------------------------------------------------------
/project/server/functions/onStripeCheckoutSessionCompleted.js:
--------------------------------------------------------------------------------
1 | import staqConfig from "../../staq";
2 | import { getSecret } from "../util";
3 |
4 | const functions = require("firebase-functions");
5 | const _stripe = require("stripe");
6 |
7 | let endpointSecret;
8 | let fulfillOrder;
9 |
10 | async function onStripeCheckoutSessionCompleted(req, res, stripe) {
11 | const payload = req.rawBody;
12 | const sig = req.headers["stripe-signature"];
13 |
14 | let event;
15 |
16 | try {
17 | event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
18 | } catch (err) {
19 | console.error(err);
20 | return res.status(400).send(`Webhook Error: ${err.message}`);
21 | }
22 |
23 | if (event.type === "checkout.session.completed") {
24 | const session = event.data.object;
25 | fulfillOrder(session, () => {
26 | console.log("inside callback");
27 | res.status(200).end();
28 | });
29 | } else {
30 | res.status(200).end();
31 | }
32 | }
33 |
34 | export default functions.https.onRequest(async (req, res) => {
35 | const stripeSecretKey = await getSecret("stripe-secret-key");
36 | const stripe = _stripe(stripeSecretKey);
37 |
38 | const secretName = staqConfig.get("stripeCheckoutSessionCompletedSecretName");
39 | endpointSecret = await getSecret(secretName);
40 |
41 | fulfillOrder = staqConfig.get("stripeFulfillOrder");
42 |
43 | onStripeCheckoutSessionCompleted(req, res, stripe);
44 | });
45 |
--------------------------------------------------------------------------------
/project/client/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import commonjs from 'rollup-plugin-commonjs'
3 | import postcss from 'rollup-plugin-postcss'
4 | import resolve from 'rollup-plugin-node-resolve'
5 |
6 | const extensions = ['.js', '.jsx', '.ts', '.tsx']
7 |
8 | export default {
9 | input: './src/index.js',
10 | output: {
11 | file: './dist/index.js',
12 | format: 'esm',
13 | },
14 | plugins: [
15 | postcss({
16 | config: {
17 | path: './postcss.config.js',
18 | },
19 | extensions: ['.css'],
20 | minimize: true,
21 | inject: {
22 | insertAt: 'top',
23 | },
24 | }),
25 | resolve({
26 | mainFields: ['module', 'main', 'jsnext:main', 'browser'],
27 | extensions,
28 | }),
29 | babel({
30 | exclude: './node_modules/**',
31 | extensions,
32 | }),
33 | commonjs({
34 | include: 'node_modules/**',
35 | // left-hand side can be an absolute path, a path
36 | // relative to the current directory, or the name
37 | // of a module in node_modules
38 | namedExports: {
39 | 'node_modules/react/index.js': [
40 | 'cloneElement',
41 | 'createContext',
42 | 'Component',
43 | 'createElement',
44 | ],
45 | 'node_modules/react-dom/index.js': ['render', 'hydrate'],
46 | 'node_modules/react-is/index.js': [
47 | 'isFragment',
48 | 'isElement',
49 | 'isValidElementType',
50 | 'ForwardRef',
51 | 'Memo',
52 | ],
53 | },
54 | }),
55 | ],
56 | external: [
57 | 'react',
58 | 'react-dom',
59 | 'react-router-dom',
60 | 'firebase/app',
61 | 'firebase/analytics',
62 | 'firebase/auth',
63 | 'firebase/functions',
64 | 'firebase/firestore',
65 | ],
66 | }
67 |
--------------------------------------------------------------------------------
/project/client/src/lib/withStripe.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { loadStripe } from '@stripe/stripe-js'
3 | import { Elements, useStripe } from '@stripe/react-stripe-js'
4 | import staqConfig from '../../../staq'
5 |
6 | const noop = () => {}
7 |
8 | const StripeContext = React.createContext(null)
9 |
10 | export const withStripe = (Component) => (props) => (
11 |
12 | {(stripeContext) => }
13 |
14 | )
15 |
16 | const StripeProviderBase = (props) => {
17 | const stripe = useStripe()
18 | const { children } = props
19 | return (
20 |
25 | {children}
26 |
27 | )
28 | }
29 |
30 | export const getStripeCheckoutSession = (
31 | clientReferenceId,
32 | stripeCustomerId,
33 | priceId,
34 | ) => {
35 | const firebase = staqConfig.get('firebase')
36 | const successUrl = staqConfig.get('Payments.CheckoutSuccessUrl')
37 | const cancelUrl = staqConfig.get('Payments.CheckoutCancelUrl')
38 | const createCheckoutSession = firebase.functions.httpsCallable(
39 | 'createStripeCheckoutSession',
40 | )
41 | return createCheckoutSession({
42 | clientReferenceId,
43 | priceId,
44 | successUrl,
45 | cancelUrl,
46 | customerId: stripeCustomerId,
47 | })
48 | }
49 |
50 | export default (props) => {
51 | const { children } = props
52 | const usePayments = staqConfig.get('Payments.Enabled')
53 | const stripePromise = usePayments
54 | ? loadStripe(staqConfig.get('Payments.StripePublishableKey'))
55 | : new Promise(noop)
56 | return (
57 |
58 | {children}
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/project/client/src/components/Pricing/PlanCardOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | function PlanCardOne(props) {
5 | const { Title, Subtitle, Price, Features, CtaLink } = props
6 |
7 | return (
8 |
13 |
14 |
{Title}
15 |
{Subtitle}
16 |
17 |
18 |
19 |
20 | {' '}
21 | {Price.Value}{' '}
22 |
23 |
28 | {Price.Description}{' '}
29 |
30 |
31 |
{Price.Subdescription}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {Features.map((feature) => (
40 | -
41 | {feature.Text}
42 |
43 | ))}
44 |
45 |
46 |
52 | {CtaLink.Text}
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default PlanCardOne
60 |
--------------------------------------------------------------------------------
/project/client/src/components/Hero/HeroOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | function HeroOne(props) {
5 | const {
6 | PrimaryText,
7 | SecondaryText,
8 | Image,
9 | PrimaryLink,
10 | SecondaryLink,
11 | } = props
12 |
13 | return (
14 |
15 |
20 |
21 |
22 | {PrimaryText}
23 |
24 |
25 |
26 | {SecondaryText}
27 |
28 |
29 |
30 | {PrimaryLink && (
31 |
37 | {PrimaryLink.Text}
38 |
39 | )}
40 |
41 | {SecondaryLink && (
42 |
48 | {SecondaryLink.Text}
49 |
50 | )}
51 |
52 |
53 |
54 |

55 |
56 |
57 |
58 | )
59 | }
60 |
61 | export default HeroOne
62 |
--------------------------------------------------------------------------------
/project/server/functions/stripeCustomer.js:
--------------------------------------------------------------------------------
1 | import staqConfig from "../../staq";
2 | import { getSecret } from "../util";
3 |
4 | const functions = require("firebase-functions");
5 | const _stripe = require("stripe");
6 |
7 | async function createSubscription(customer) {
8 | try {
9 | const subscriptionObj = {
10 | customer: customer.id,
11 | items: [{ price: staqConfig.get("stripeTrialPriceId") }],
12 | };
13 |
14 | if (staqConfig.get("useTrial")) {
15 | subscriptionObj.trial_period_days =
16 | staqConfig.get("stripeTrialPeriodDays") || 14;
17 | }
18 |
19 | const subscription = await stripe.subscriptions.create(subscriptionObj);
20 |
21 | return subscription;
22 | } catch (error) {
23 | console.error(error);
24 | throw new Error(error.message);
25 | }
26 | }
27 |
28 | async function createCustomer(data, context, stripe) {
29 | try {
30 | const customer = await stripe.customers.create(data.customer);
31 |
32 | const subscription =
33 | staqConfig.get("paymentType") === "subscription"
34 | ? createSubscription(customer)
35 | : null;
36 |
37 | return {
38 | customer,
39 | subscription,
40 | };
41 | } catch (error) {
42 | console.error(error);
43 | return {
44 | error,
45 | };
46 | }
47 | }
48 |
49 | async function getCustomer(data, context, stripe) {
50 | try {
51 | const customer = await stripe.customers.retrieve(data.customerId);
52 | console.log("retrieved customer", customer);
53 | return customer;
54 | } catch (error) {
55 | console.error("error retrieving customer", err);
56 | return {
57 | error,
58 | };
59 | }
60 | }
61 |
62 | export default functions.https.onCall(async (data, context) => {
63 | console.log("data", data);
64 |
65 | const stripeSecretKey = await getSecret("stripe-secret-key");
66 | const stripe = _stripe(stripeSecretKey);
67 |
68 | if (data.action === "create") {
69 | return createCustomer(data, context, stripe);
70 | } else if (data.action === "get") {
71 | return getCustomer(data, context, stripe);
72 | }
73 | });
74 |
--------------------------------------------------------------------------------
/project/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@staqjs/client",
3 | "version": "0.0.32",
4 | "description": "StaqJS is a Javascript library for creating Software-as-a-Service (SaaS) businesses",
5 | "main": "dist/index.js",
6 | "source": "src/index.js",
7 | "engines": {
8 | "node": ">=10"
9 | },
10 | "scripts": {
11 | "build": "NODE_ENV=production rollup -c",
12 | "start": "rollup --watch -c",
13 | "prepare": "run-s build",
14 | "test": "run-s test:unit test:lint test:build",
15 | "test:build": "run-s build",
16 | "test:lint": "eslint ."
17 | },
18 | "files": [
19 | "dist"
20 | ],
21 | "dependencies": {
22 | "@stripe/react-stripe-js": "^1.1.2",
23 | "@stripe/stripe-js": "^1.8.0",
24 | "react-icons": "^4.2.0",
25 | "react-portal": "^4.2.1",
26 | "react-tooltip-controller": "^1.0.6"
27 | },
28 | "peerDependencies": {
29 | "firebase": "^7.16.1",
30 | "react": "^16.13.1",
31 | "react-router": "^5.2.0",
32 | "react-router-dom": "^5.2.0"
33 | },
34 | "devDependencies": {
35 | "autoprefixer": "^10.2.5",
36 | "babel-eslint": "^10.1.0",
37 | "cross-env": "^7.0.2",
38 | "eslint": "^7.5.0",
39 | "eslint-config-airbnb": "^18.2.0",
40 | "eslint-plugin-import": "^2.22.0",
41 | "eslint-plugin-jsx-a11y": "^6.3.1",
42 | "eslint-plugin-react": "^7.20.3",
43 | "eslint-plugin-react-hooks": "^4.0.8",
44 | "npm-run-all": "^4.1.5",
45 | "postcss": "^8.2.7",
46 | "prettier": "^2.0.4",
47 | "react-scripts": "^3.4.1",
48 | "rollup": "^2.40.0",
49 | "rollup-plugin-babel": "^4.4.0",
50 | "rollup-plugin-commonjs": "^10.1.0",
51 | "rollup-plugin-node-resolve": "^5.2.0",
52 | "rollup-plugin-postcss": "^4.0.0",
53 | "rollup-plugin-tailwindcss": "^1.0.0",
54 | "tailwindcss": "^2.0.3"
55 | },
56 | "repository": {
57 | "type": "git",
58 | "url": "git+https://github.com/staqjs/staq.git"
59 | },
60 | "author": "Matt Roll",
61 | "license": "MIT",
62 | "bugs": {
63 | "url": "https://github.com/staqjs/staq/issues"
64 | },
65 | "homepage": "https://github.com/staqjs/staq#readme"
66 | }
67 |
--------------------------------------------------------------------------------
/project/client/src/lib/Auth/context.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { withFirebase } from '../Firebase'
4 |
5 | const AuthContext = React.createContext(null)
6 |
7 | export const withAuth = (Component) => (props) => (
8 |
9 | {(auth) => }
10 |
11 | )
12 |
13 | class AuthProvider extends React.Component {
14 | constructor(props) {
15 | super(props)
16 |
17 | this.state = {
18 | currentUser: JSON.parse(localStorage.getItem('currentUser')),
19 | onLogoutCallback: () => {
20 | console.log('logging out')
21 | },
22 | }
23 | }
24 |
25 | componentDidMount() {
26 | this.listener = this.props.firebase.onAuthUserListener(
27 | (currentUser) => {
28 | localStorage.setItem('currentUser', JSON.stringify(currentUser))
29 | this.setState({ currentUser })
30 | },
31 | () => {
32 | localStorage.removeItem('currentUser')
33 | this.setState({ currentUser: null })
34 | this.state.onLogoutCallback()
35 | },
36 | )
37 | }
38 |
39 | componentWillUnmount() {
40 | this.listener()
41 | }
42 |
43 | reload = () => {
44 | this.setState({
45 | currentUser: JSON.parse(localStorage.getItem('currentUser')),
46 | })
47 | }
48 |
49 | update = (newFields) => {
50 | const { firebase } = this.props
51 |
52 | firebase
53 | .user(this.state.currentUser.uid)
54 | .update(newFields)
55 | .then(() => {
56 | const currentUser = {
57 | ...this.state.currentUser,
58 | ...newFields,
59 | }
60 | localStorage.setItem('currentUser', JSON.stringify(currentUser))
61 | this.setState({ currentUser })
62 | })
63 | }
64 |
65 | onLogout = (newCallbackOnLogout) => {
66 | this.setState({
67 | onLogoutCallback: newCallbackOnLogout,
68 | })
69 | }
70 |
71 | render() {
72 | const { children } = this.props
73 | const { currentUser } = this.state
74 |
75 | return (
76 |
84 | {children}
85 |
86 | )
87 | }
88 | }
89 |
90 | export default withFirebase(AuthProvider)
91 |
--------------------------------------------------------------------------------
/project/client/src/components/Footer/FooterOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { Portal } from 'react-portal'
4 | import _ from 'lodash'
5 |
6 | function FooterColumn(props) {
7 | const { Title, Links } = props
8 |
9 | return (
10 |
11 |
12 | {Title}
13 |
14 |
36 |
37 | )
38 | }
39 |
40 | function PoweredByStaqLabel() {
41 | return (
42 |
51 | )
52 | }
53 |
54 | function FooterOne(props) {
55 | const { Columns, Copyright, PoweredByStaq } = props
56 |
57 | return (
58 |
59 |
64 |
65 | {Columns.map((column) => (
66 |
71 | ))}
72 |
73 |
74 |
75 |
76 |
77 | © {Copyright}
78 |
79 |
80 |
81 | {PoweredByStaq || false ?
: null}
82 |
83 |
84 |
85 | )
86 | }
87 |
88 | export default FooterOne
89 |
--------------------------------------------------------------------------------
/project/client/src/pages/ForgotPasswordPage/ForgotPasswordPageOne.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link, Redirect } from 'react-router-dom'
3 |
4 | import { withFirebase } from '../../lib/Firebase'
5 | import { withAuth } from '../../lib/Auth'
6 | import staqConfig from '../../../../staq'
7 |
8 | function ForgotPasswordPageBase(props) {
9 | const { auth, firebase } = props
10 | const Logo = staqConfig.get('logo') || null
11 |
12 | return auth.currentUser ? (
13 |
14 | ) : (
15 |
16 | {Logo ? (
17 |
18 |
19 |
20 | ) : null}
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | function ForgotPasswordForm(props) {
28 | const { firebase } = props
29 |
30 | const [email, setEmail] = React.useState('')
31 | const [error, setError] = React.useState('')
32 |
33 | const onSubmit = (event) => {
34 | firebase.auth
35 | .sendPasswordResetEmail(email)
36 | .then(() => {
37 | setError(`Sent reset instructions to ${email}`)
38 | })
39 | .catch(() => {
40 | setError('Error sending reset email')
41 | })
42 | event.preventDefault()
43 | }
44 |
45 | const onChangeEmail = (event) => {
46 | setEmail(event.target.value)
47 | }
48 |
49 | return (
50 |
53 | Reset Password
54 |
55 |
75 |
76 | )
77 | }
78 |
79 | function ForgotPasswordPage(props) {
80 | return
81 | }
82 |
83 | export default withFirebase(withAuth(ForgotPasswordPage))
84 |
--------------------------------------------------------------------------------
/project/client/src/lib/signup.js:
--------------------------------------------------------------------------------
1 | import staqConfig from '../../../staq'
2 |
3 | const noop = () => {}
4 |
5 | async function createStripeCustomer(email, onSuccess, onError) {
6 | const firebase = staqConfig.get('firebase')
7 | const stripeCustomerFn = firebase.functions.httpsCallable('stripeCustomer')
8 | stripeCustomerFn({
9 | action: 'create',
10 | customer: { email },
11 | })
12 | .then((result) => {
13 | onSuccess(result)
14 | })
15 | .catch(onError)
16 | }
17 |
18 | const getStripeAttributes = (stripeData) => {
19 | const usePayments = staqConfig.get('Payments.Enabled')
20 | const paymentType = staqConfig.get('Payments.Type')
21 | const stripeAttributes = {}
22 |
23 | if (usePayments) {
24 | stripeAttributes.stripeCustomerId = stripeData.customer.id
25 |
26 | if (paymentType === 'subscription') {
27 | stripeAttributes.subscriptionPriceId =
28 | stripeData.subscription.items.data[0].price.id
29 | }
30 | }
31 |
32 | return stripeAttributes
33 | }
34 |
35 | const createFirebaseUser = (
36 | email,
37 | password,
38 | stripeData,
39 | onSuccess,
40 | onError,
41 | ) => {
42 | const firebase = staqConfig.get('firebase')
43 | const stripeAttributes = getStripeAttributes(stripeData)
44 | const userDefaults = staqConfig.get('UserDefaults', {})
45 |
46 | firebase
47 | .doCreateUserWithEmailAndPassword(email, password)
48 | .then((currentUser) => {
49 | const userData = {
50 | email,
51 | uid: currentUser.user.uid,
52 | ...stripeAttributes,
53 | ...userDefaults,
54 | }
55 | return firebase
56 | .user(currentUser.user.uid)
57 | .set(userData)
58 | .then(() => {
59 | onSuccess(userData)
60 | })
61 | })
62 | .catch(onError)
63 | }
64 |
65 | const submitWithPaymentsEnabled = (userInfo, onSuccess, onError) => {
66 | createStripeCustomer(userInfo.email, (response) => {
67 | if (response.statusCode && response.statusCode !== 200) {
68 | onError(response.code)
69 | } else {
70 | createFirebaseUser(
71 | userInfo.email,
72 | userInfo.password,
73 | response.data,
74 | onSuccess,
75 | )
76 | }
77 | })
78 | }
79 |
80 | const submitWithPaymentsDisabled = (
81 | userInfo,
82 | onSuccess = noop,
83 | onError = noop,
84 | ) => {
85 | createFirebaseUser(
86 | userInfo.email,
87 | userInfo.password,
88 | null,
89 | onSuccess,
90 | onError,
91 | )
92 | }
93 |
94 | export const signup = (userInfo, onSuccess = noop, onError = noop) => {
95 | const submitFn = staqConfig.get('Payments.Enabled')
96 | ? submitWithPaymentsEnabled
97 | : submitWithPaymentsDisabled
98 |
99 | submitFn(userInfo, onSuccess, onError)
100 | }
101 |
--------------------------------------------------------------------------------
/project/client/src/lib/Firebase/firebase.js:
--------------------------------------------------------------------------------
1 | import app from 'firebase/app'
2 | import 'firebase/analytics'
3 | import 'firebase/auth'
4 | import 'firebase/firestore'
5 | import 'firebase/functions'
6 |
7 | import staqConfig from '../../../../staq'
8 |
9 | class Firebase {
10 | constructor(config) {
11 | app.initializeApp(config)
12 |
13 | this.auth = app.auth()
14 | this.db = app.firestore()
15 | this.analytics = app.analytics()
16 | this.functions = app.functions()
17 |
18 | if (window.location.host.includes('localhost')) {
19 | this.functions.useFunctionsEmulator('http://localhost:5001')
20 | }
21 | }
22 |
23 | logEvent = (eventName, eventParams) => {
24 | this.analytics.logEvent(eventName, eventParams)
25 | }
26 |
27 | // *** Auth API ***
28 | doCreateUserWithEmailAndPassword = (email, password) =>
29 | this.auth.createUserWithEmailAndPassword(email, password)
30 |
31 | doSignInWithEmailAndPassword = (email, password) =>
32 | this.auth.signInWithEmailAndPassword(email, password)
33 |
34 | doSignOut = () => this.auth.signOut()
35 |
36 | doPasswordReset = (email) => this.auth.sendPasswordResetEmail(email)
37 |
38 | doPasswordUpdate = (password) =>
39 | this.auth.currentUser.updatePassword(password)
40 |
41 | doSendEmailVerification = () =>
42 | this.auth.currentUser.sendEmailVerification({
43 | url: staqConfig.get('BaseUrl')
44 | })
45 |
46 | // *** Merge Auth and DB User API *** //
47 |
48 | onAuthUserListener = (next, fallback) =>
49 | this.auth.onAuthStateChanged((currentUser) => {
50 | if (currentUser) {
51 | this.user(currentUser.uid)
52 | .get()
53 | .then((snapshot) => {
54 | const dbUser = snapshot.data()
55 |
56 | // merge auth and db user
57 | const user = {
58 | uid: currentUser.uid,
59 | email: currentUser.email,
60 | emailVerified: currentUser.emailVerified,
61 | providerData: currentUser.providerData,
62 | ...dbUser
63 | }
64 |
65 | next(user)
66 | })
67 | } else {
68 | fallback()
69 | }
70 | })
71 |
72 | // *** User API ***
73 |
74 | user = (uid) => this.db.collection('users').doc(uid)
75 |
76 | users = () => this.db.collection('users')
77 |
78 | collection = (collectionName) => this.db.collection(collectionName)
79 |
80 | collectionForUser = (collectionName, uid) =>
81 | this.collection(collectionName).where('uid', '==', uid)
82 |
83 | document = (collectionName, documentId) =>
84 | this.collection(collectionName).doc(documentId)
85 |
86 | addDocumentForUser = (uid, collectionName, document) =>
87 | this.db.collection(collectionName).add({
88 | uid,
89 | ...document
90 | })
91 |
92 | updateDocument = (collectionName, documentId, updateFields) => {
93 | return this.db
94 | .collection(collectionName)
95 | .doc(documentId)
96 | .update(updateFields)
97 | }
98 | }
99 |
100 | export default Firebase
101 |
--------------------------------------------------------------------------------
/project/client/src/components/SignInForm/SignInFormOne.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Link, useHistory } from 'react-router-dom'
3 |
4 | import { withFirebase } from '../../lib/Firebase'
5 | import * as Routes from '../../constants/routes'
6 |
7 | function SignInFormOne(props) {
8 | const history = useHistory()
9 | const [state, setState] = useState({
10 | email: '',
11 | password: '',
12 | })
13 | const { firebase } = props
14 | const [loading, setLoading] = useState(false)
15 | const [error, setError] = useState(null)
16 |
17 | const setField = (field, value) => {
18 | setState({
19 | ...state,
20 | [field]: value,
21 | })
22 | }
23 |
24 | const onSubmit = (e) => {
25 | firebase
26 | .doSignInWithEmailAndPassword(state.email, state.password)
27 | .then(() => {
28 | setState({
29 | email: '',
30 | password: '',
31 | })
32 | })
33 | .catch((error) => {
34 | setError('Please enter a valid username and password.')
35 | })
36 |
37 | e.preventDefault()
38 | }
39 |
40 | return (
41 |
46 |
Sign In
47 |
48 |
91 |
92 | )
93 | }
94 |
95 | export default withFirebase(SignInFormOne)
96 |
--------------------------------------------------------------------------------
/project/client/src/lib/StaqRoutes.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Redirect, Route, useLocation, useHistory } from 'react-router-dom'
3 | import _ from 'lodash'
4 |
5 | import staqConfig from '../../../staq'
6 |
7 | import { withAuth } from './Auth'
8 |
9 | import LandingPage from '../pages/LandingPage/LandingPage'
10 | import SignUpPage from '../pages/SignUpPage/SignUpPage'
11 | import SignInPage from '../pages/SignInPage/SignInPage'
12 | import PricingPage from '../pages/PricingPage/PricingPage'
13 | import ForgotPasswordPage from '../pages/ForgotPasswordPage/ForgotPasswordPage'
14 |
15 | import * as Footers from '../components/Footer'
16 | import * as Navbars from '../components/Navigation'
17 | import * as Routes from '../constants/routes'
18 |
19 | function ScrollToTop() {
20 | const { pathname } = useLocation()
21 |
22 | useEffect(() => {
23 | window.scrollTo(0, 0)
24 | }, [pathname])
25 |
26 | return null
27 | }
28 |
29 | function PrivateRouteBase({ component: Component, auth, ...rest }) {
30 | return (
31 | {
34 | return auth.currentUser ? (
35 |
36 | ) : (
37 |
38 | )
39 | }}
40 | />
41 | )
42 | }
43 |
44 | const footerRoutes = [
45 | '/',
46 | '/demo',
47 | '/signin',
48 | '/signup',
49 | '/pricing',
50 | '/forgot-password',
51 | ]
52 |
53 | function StaqRoutes() {
54 | const history = useHistory()
55 | const [pathname, setPathname] = useState(history.location.pathname)
56 |
57 | history.listen((location) => {
58 | setPathname(location.pathname)
59 | })
60 |
61 | const template = staqConfig.get('Template.Name')
62 |
63 | const navbarRoutes = staqConfig.get('navbarRoutes')
64 | const Navbar = Navbars[`Navbar${template}`]
65 |
66 | const footerColumns = staqConfig.get('Template.Config.Footer.Columns', {})
67 | const copyright = staqConfig.get('Template.Config.Footer.Copyright')
68 | const poweredByStaq = staqConfig.get(
69 | 'Template.Config.Footer.PoweredByStaq',
70 | false,
71 | )
72 | const Footer = Footers[`Footer${template}`]
73 |
74 | return (
75 | <>
76 |
77 | {_.isNil(navbarRoutes) ? (
78 |
79 | ) : navbarRoutes.includes(pathname) ? (
80 |
81 | ) : null}
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {footerRoutes.includes(pathname) ? (
91 |
96 | ) : null}
97 | >
98 | )
99 | }
100 |
101 | const PrivateRoute = withAuth(PrivateRouteBase)
102 |
103 | export default StaqRoutes
104 | export { PrivateRoute }
105 |
--------------------------------------------------------------------------------
/project/client/src/components/SignUpForm/SignUpFormOne.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { useHistory } from 'react-router-dom'
3 |
4 | import { signup } from '../../lib/signup'
5 |
6 | function SignUpFormOne(props) {
7 | const history = useHistory()
8 | const [state, setState] = useState({
9 | email: '',
10 | password: '',
11 | passwordConfirmation: '',
12 | })
13 | const [loading, setLoading] = useState(false)
14 | const [error, setError] = useState(null)
15 |
16 | const setField = (field, value) => {
17 | setState({
18 | ...state,
19 | [field]: value,
20 | })
21 | }
22 |
23 | const onSubmit = (e) => {
24 | setLoading(true)
25 | signup(
26 | state,
27 | (res) => {
28 | setLoading(false)
29 | history.push('/dashboard')
30 | },
31 | (error) => {
32 | setError(error.message)
33 | },
34 | )
35 |
36 | e.preventDefault()
37 | }
38 |
39 | return (
40 |
45 |
Sign Up
46 |
47 |
98 |
99 | )
100 | }
101 |
102 | export default SignUpFormOne
103 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [matt@staqjs.com](mailto:matt@staqjs.com). All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # staq
2 |
3 | Staq is a Javascript library for creating Software-as-a-Service (SaaS) businesses.
4 |
5 |
6 | The `staq` package contains a set of [React](https://reactjs.org/) components that implement standard SaaS features including:
7 |
8 | - User accounts
9 | - Landing page
10 | - Pricing page
11 | - Subscription management (via Stripe [Customer Portal](https://stripe.com/docs/billing/subscriptions/customer-portal))
12 |
13 | The package also ships with a set of NodeJS functions to be used with Google [Firebase](https://firebase.google.com/) to implement the backend logic necessary for features like subscription billing with [Stripe](https://stripe.com/).
14 |
15 | # Project Maturity
16 |
17 | Staq is a new project that has yet to undergo thorough testing in the wild. It is in production use at [checkfox.app](https://checkfox.app), but should generally be considered fragile and unpredictable.
18 |
19 | It is currently most valuable for the weekend project that you want to get online as fast as possible, but not for supporting your existing business with many paying customers. We will get there soon! But for now it makes sense to think of Staq as a [toy](https://twitter.com/paulg/status/1075675559865270273?lang=en).
20 |
21 | # Quickstart
22 |
23 | Staq comes in two parts, a client side and a server side library. This
24 | guide will show you how to create a basic SaaS app from scratch using
25 | Staq for both sides. It will take five or ten minutes.
26 |
27 | Note: If you don’t have a Stripe account or don’t want to enable
28 | payments, you can skip the payments configuration in index.js and the
29 | entire Server section.
30 |
31 | ## Client
32 |
33 | 1. First let’s create a React project with `create-react-app`.
34 |
35 | ```sh
36 | npx create-react-app staq-quickstart && cd staq-quickstart
37 | ```
38 |
39 | 2. Then let’s install dependencies. We need the Staq.js client side
40 | library, Firebase, and React Router.
41 |
42 | ```sh
43 | yarn add @staqjs/client firebase react-router-dom
44 | ```
45 |
46 | 3. To install Staq into our app, we need to add some code to two
47 | files. The first is index.js, where we configure the library and wrap
48 | our app in a Staq function call. This is what the updated index.js
49 | should look like.
50 |
51 | ```jsx
52 | // src/index.js
53 |
54 | import React from 'react';
55 | import ReactDOM from 'react-dom';
56 | import './index.css';
57 | import App from './App';
58 | import * as serviceWorker from './serviceWorker';
59 | import { initStaq, withStaq } from '@staqjs/client'
60 |
61 | // Pass in configuration options
62 | initStaq({
63 | SiteTitle: 'Storyblanks',
64 | Template: {
65 | Name: 'One',
66 | Config: {
67 | Navbar: {
68 | MenuLinks: [
69 | { to: '/pricing', text: 'Pricing' },
70 | { to: '/signin', text: 'Login' },
71 | ],
72 | GetStartedLink: '/signup',
73 | },
74 | Hero: {
75 | PrimaryText: 'SaaS apps are great!',
76 | SecondaryText: 'You should totally subscribe',
77 | PrimaryLink: { text: 'Get Started', to: '/signup' },
78 | },
79 | },
80 | },
81 | FirebaseConfig: {
82 | // your Firebase config object
83 | },
84 | })
85 |
86 | ReactDOM.render(
87 | // Wrap the application with a Staq.js context
88 | withStaq(
89 |
90 |
91 |
92 | ),
93 | document.getElementById('root')
94 | );
95 |
96 | // If you want your app to work offline and load faster, you can change
97 | // unregister() to register() below. Note this comes with some pitfalls.
98 | // Learn more about service workers: https://bit.ly/CRA-PWA
99 | serviceWorker.unregister();
100 | ```
101 |
102 | The other file we need to update is `App.js`. This is where we use
103 | Staq to render basic routes like landing page, sign in page, sign
104 | out page, etc.
105 |
106 | Remove the markup in that file, and replace it with a Router and a
107 | Staq component. Here’s what the updated file should look like.
108 |
109 | ```jsx
110 | // src/App.js
111 |
112 | import React from 'react';
113 | import logo from './logo.svg';
114 | import './App.css';
115 | import { Router } from 'react-router-dom'
116 | import { StaqRoutes } from '@staqjs/client'
117 |
118 | function App() {
119 | return (
120 |
121 |
122 |
123 | );
124 | }
125 |
126 | export default App;
127 | ```
128 |
129 | 4. That’s it! On the client side anyway. Start up your app with `yarn
130 | start` and check out the pages at `/signup`, `/signin`.
131 |
132 |
133 | ## Server
134 |
135 | 1. If you don’t have the Firebase CLI yet, follow [this guide](https://firebase.google.com/docs/cli) to install it.
136 |
137 | 2. Initialize Firebase in the `staq-quickstart` project.
138 |
139 | ```sh
140 | firebase init
141 | ```
142 |
143 | 3. Install the Staq server side library.
144 |
145 | ```sh
146 | yarn add @staqjs/server
147 | ```
148 |
149 | 4. Add Staq server code to functions/index.js to support everything
150 | needed for subscription payments with Stripe. Replace what’s in the
151 | file with the following.
152 |
153 | ```js
154 | const { initStaq, createStripeCustomerPortalSession, stripeCustomer } = require('@staqjs/server')
155 |
156 | initStaq({
157 | gcpProjectNumber: // your Google Cloud project number
158 | stripeTrialPriceId: // the ID of the default Stripe Price users will be subscribed to
159 | useTrial: false, // whether or not user should be started on a trial
160 | })
161 |
162 | exports.stripeCustomer = stripeCustomer
163 | exports.createStripeCustomerPortalSession = createStripeCustomerPortalSession
164 | ```
165 |
166 | 5. Deploy your Firebase functions.
167 |
168 | ```sh
169 | firebase deploy --only functions
170 | ```
171 |
172 |
173 |
174 | ## License
175 |
176 | MIT © [mroll](https://github.com/mroll)
177 |
--------------------------------------------------------------------------------
/project/client/src/components/Navigation/NavbarOne.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { Select, ToolTipController } from 'react-tooltip-controller'
3 | import { FiMenu } from 'react-icons/fi'
4 | import { Link, useHistory } from 'react-router-dom'
5 |
6 | import staqConfig from '../../../../staq'
7 | import { withAuth } from '../../lib/Auth'
8 | import { withFirebase } from '../../lib/Firebase'
9 |
10 | function RegularSizeNavbar(props) {
11 | const history = useHistory()
12 | const { auth, firebase } = props
13 |
14 | const Logo = staqConfig.get('Logo')
15 | const SiteTitle = staqConfig.get('SiteTitle')
16 | const MenuLinks = staqConfig.get('Template.Config.Navbar.MenuLinks', [])
17 | const GetStartedLink = staqConfig.get(
18 | 'Template.Config.Navbar.GetStartedLink',
19 | '/signup',
20 | )
21 |
22 | const onClickSignOut = () => {
23 | auth.onLogout(() => {
24 | history.push('/')
25 | })
26 | firebase.doSignOut()
27 | }
28 |
29 | return (
30 |
31 |
36 |
37 |
38 | {Logo &&
}
39 | {SiteTitle}
40 |
41 |
42 |
43 | {auth.currentUser ? (
44 |
45 | ) : (
46 |
53 | )}
54 |
55 | {auth.currentUser ? (
56 |
57 |
65 |
66 | ) : (
67 |
68 |
74 | Get Started
75 |
76 |
77 | )}
78 |
79 |
80 | )
81 | }
82 |
83 | function NonAuthMenu(props) {
84 | const history = useHistory()
85 | const { MenuLinks } = props
86 |
87 | const [anchorEl, setAnchorEl] = useState(null)
88 |
89 | const handleClick = (event) => {
90 | setAnchorEl(event.currentTarget)
91 | }
92 |
93 | const handleClose = () => {
94 | setAnchorEl(null)
95 | }
96 |
97 | const onClickMenuLink = (menuLink) => {
98 | history.push(menuLink.to)
99 | handleClose()
100 | }
101 |
102 | return (
103 |
104 |
105 |
110 |
111 |
116 | {MenuLinks.map((menuLink) => (
117 |
118 | {menuLink.Text}
119 |
120 | ))}
121 |
122 |
123 |
124 | )
125 | }
126 |
127 | function AuthMenu(props) {
128 | const history = useHistory()
129 | const { auth, firebase } = props
130 | const [anchorEl, setAnchorEl] = useState(null)
131 |
132 | const handleClick = (event) => {
133 | setAnchorEl(event.currentTarget)
134 | }
135 |
136 | const handleClose = () => {
137 | setAnchorEl(null)
138 | }
139 |
140 | const onClickSignOut = () => {
141 | auth.onLogout(() => {
142 | history.push('/')
143 | })
144 | firebase.doSignOut()
145 | }
146 |
147 | return (
148 |
149 |
150 |
155 |
156 |
161 |
162 |
163 |
164 |
165 | )
166 | }
167 |
168 | function SmallScreenavbar(props) {
169 | const { auth, firebase } = props
170 |
171 | const Logo = staqConfig.get('Logo')
172 | const SiteTitle = staqConfig.get('SiteTitle')
173 | const MenuLinks = staqConfig.get('Template.Config.Navbar.MenuLinks', [])
174 | const GetStartedLink = staqConfig.get(
175 | 'Template.Config.Navbar.GetStartedLink',
176 | '/signup',
177 | )
178 |
179 | return (
180 |
185 |
186 |
187 | {Logo &&
}
188 | {SiteTitle}
189 |
190 |
191 |
192 | {auth.currentUser ? (
193 |
194 | ) : (
195 |
196 | )}
197 |
198 | )
199 | }
200 |
201 | function NavBarOne(props) {
202 | const [smScreen, setSmScreen] = useState(window.innerWidth < 450)
203 |
204 | const updateMedia = () => {
205 | setSmScreen(window.innerWidth < 450)
206 | }
207 |
208 | useEffect(() => {
209 | window.addEventListener('resize', updateMedia)
210 | return () => window.removeEventListener('resize', updateMedia)
211 | })
212 |
213 | return smScreen ? (
214 |
215 | ) : (
216 |
217 | )
218 | }
219 |
220 | export default withFirebase(withAuth(NavBarOne))
221 |
--------------------------------------------------------------------------------
/project/client/tailwind.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 |
3 | module.exports = {
4 | purge: [],
5 | presets: [],
6 | darkMode: false, // or 'media' or 'class'
7 | theme: {
8 | screens: {
9 | sm: '640px',
10 | md: '768px',
11 | lg: '1024px',
12 | xl: '1280px',
13 | '2xl': '1536px',
14 | },
15 | colors: {
16 | transparent: 'transparent',
17 | current: 'currentColor',
18 |
19 | black: colors.black,
20 | white: colors.white,
21 | gray: colors.coolGray,
22 | red: colors.red,
23 | yellow: colors.amber,
24 | green: colors.emerald,
25 | blue: colors.blue,
26 | indigo: colors.indigo,
27 | purple: colors.violet,
28 | pink: colors.pink,
29 | },
30 | spacing: {
31 | px: '1px',
32 | 0: '0px',
33 | 0.5: '0.125rem',
34 | 1: '0.25rem',
35 | 1.5: '0.375rem',
36 | 2: '0.5rem',
37 | 2.5: '0.625rem',
38 | 3: '0.75rem',
39 | 3.5: '0.875rem',
40 | 4: '1rem',
41 | 5: '1.25rem',
42 | 6: '1.5rem',
43 | 7: '1.75rem',
44 | 8: '2rem',
45 | 9: '2.25rem',
46 | 10: '2.5rem',
47 | 11: '2.75rem',
48 | 12: '3rem',
49 | 14: '3.5rem',
50 | 16: '4rem',
51 | 20: '5rem',
52 | 24: '6rem',
53 | 28: '7rem',
54 | 32: '8rem',
55 | 36: '9rem',
56 | 40: '10rem',
57 | 44: '11rem',
58 | 48: '12rem',
59 | 52: '13rem',
60 | 56: '14rem',
61 | 60: '15rem',
62 | 64: '16rem',
63 | 72: '18rem',
64 | 80: '20rem',
65 | 96: '24rem',
66 | },
67 | animation: {
68 | none: 'none',
69 | spin: 'spin 1s linear infinite',
70 | ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
71 | pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
72 | bounce: 'bounce 1s infinite',
73 | },
74 | backgroundColor: (theme) => theme('colors'),
75 | backgroundImage: {
76 | none: 'none',
77 | 'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))',
78 | 'gradient-to-tr': 'linear-gradient(to top right, var(--tw-gradient-stops))',
79 | 'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))',
80 | 'gradient-to-br': 'linear-gradient(to bottom right, var(--tw-gradient-stops))',
81 | 'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))',
82 | 'gradient-to-bl': 'linear-gradient(to bottom left, var(--tw-gradient-stops))',
83 | 'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
84 | 'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))',
85 | },
86 | backgroundOpacity: (theme) => theme('opacity'),
87 | backgroundPosition: {
88 | bottom: 'bottom',
89 | center: 'center',
90 | left: 'left',
91 | 'left-bottom': 'left bottom',
92 | 'left-top': 'left top',
93 | right: 'right',
94 | 'right-bottom': 'right bottom',
95 | 'right-top': 'right top',
96 | top: 'top',
97 | },
98 | backgroundSize: {
99 | auto: 'auto',
100 | cover: 'cover',
101 | contain: 'contain',
102 | },
103 | borderColor: (theme) => ({
104 | ...theme('colors'),
105 | DEFAULT: theme('colors.gray.200', 'currentColor'),
106 | }),
107 | borderOpacity: (theme) => theme('opacity'),
108 | borderRadius: {
109 | none: '0px',
110 | sm: '0.125rem',
111 | DEFAULT: '0.25rem',
112 | md: '0.375rem',
113 | lg: '0.5rem',
114 | xl: '0.75rem',
115 | '2xl': '1rem',
116 | '3xl': '1.5rem',
117 | full: '9999px',
118 | },
119 | borderWidth: {
120 | DEFAULT: '1px',
121 | 0: '0px',
122 | 2: '2px',
123 | 4: '4px',
124 | 8: '8px',
125 | },
126 | boxShadow: {
127 | sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
128 | DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
129 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
130 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
131 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
132 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
133 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
134 | none: 'none',
135 | },
136 | container: {},
137 | cursor: {
138 | auto: 'auto',
139 | default: 'default',
140 | pointer: 'pointer',
141 | wait: 'wait',
142 | text: 'text',
143 | move: 'move',
144 | help: 'help',
145 | 'not-allowed': 'not-allowed',
146 | },
147 | divideColor: (theme) => theme('borderColor'),
148 | divideOpacity: (theme) => theme('borderOpacity'),
149 | divideWidth: (theme) => theme('borderWidth'),
150 | fill: { current: 'currentColor' },
151 | flex: {
152 | 1: '1 1 0%',
153 | auto: '1 1 auto',
154 | initial: '0 1 auto',
155 | none: 'none',
156 | },
157 | flexGrow: {
158 | 0: '0',
159 | DEFAULT: '1',
160 | },
161 | flexShrink: {
162 | 0: '0',
163 | DEFAULT: '1',
164 | },
165 | fontFamily: {
166 | sans: [
167 | 'ui-sans-serif',
168 | 'system-ui',
169 | '-apple-system',
170 | 'BlinkMacSystemFont',
171 | '"Segoe UI"',
172 | 'Roboto',
173 | '"Helvetica Neue"',
174 | 'Arial',
175 | '"Noto Sans"',
176 | 'sans-serif',
177 | '"Apple Color Emoji"',
178 | '"Segoe UI Emoji"',
179 | '"Segoe UI Symbol"',
180 | '"Noto Color Emoji"',
181 | ],
182 | serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
183 | mono: [
184 | 'ui-monospace',
185 | 'SFMono-Regular',
186 | 'Menlo',
187 | 'Monaco',
188 | 'Consolas',
189 | '"Liberation Mono"',
190 | '"Courier New"',
191 | 'monospace',
192 | ],
193 | },
194 | fontSize: {
195 | xs: ['0.75rem', { lineHeight: '1rem' }],
196 | sm: ['0.875rem', { lineHeight: '1.25rem' }],
197 | base: ['1rem', { lineHeight: '1.5rem' }],
198 | lg: ['1.125rem', { lineHeight: '1.75rem' }],
199 | xl: ['1.25rem', { lineHeight: '1.75rem' }],
200 | '2xl': ['1.5rem', { lineHeight: '2rem' }],
201 | '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
202 | '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
203 | '5xl': ['3rem', { lineHeight: '1' }],
204 | '6xl': ['3.75rem', { lineHeight: '1' }],
205 | '7xl': ['4.5rem', { lineHeight: '1' }],
206 | '8xl': ['6rem', { lineHeight: '1' }],
207 | '9xl': ['8rem', { lineHeight: '1' }],
208 | },
209 | fontWeight: {
210 | thin: '100',
211 | extralight: '200',
212 | light: '300',
213 | normal: '400',
214 | medium: '500',
215 | semibold: '600',
216 | bold: '700',
217 | extrabold: '800',
218 | black: '900',
219 | },
220 | gap: (theme) => theme('spacing'),
221 | gradientColorStops: (theme) => theme('colors'),
222 | gridAutoColumns: {
223 | auto: 'auto',
224 | min: 'min-content',
225 | max: 'max-content',
226 | fr: 'minmax(0, 1fr)',
227 | },
228 | gridAutoRows: {
229 | auto: 'auto',
230 | min: 'min-content',
231 | max: 'max-content',
232 | fr: 'minmax(0, 1fr)',
233 | },
234 | gridColumn: {
235 | auto: 'auto',
236 | 'span-1': 'span 1 / span 1',
237 | 'span-2': 'span 2 / span 2',
238 | 'span-3': 'span 3 / span 3',
239 | 'span-4': 'span 4 / span 4',
240 | 'span-5': 'span 5 / span 5',
241 | 'span-6': 'span 6 / span 6',
242 | 'span-7': 'span 7 / span 7',
243 | 'span-8': 'span 8 / span 8',
244 | 'span-9': 'span 9 / span 9',
245 | 'span-10': 'span 10 / span 10',
246 | 'span-11': 'span 11 / span 11',
247 | 'span-12': 'span 12 / span 12',
248 | 'span-full': '1 / -1',
249 | },
250 | gridColumnEnd: {
251 | auto: 'auto',
252 | 1: '1',
253 | 2: '2',
254 | 3: '3',
255 | 4: '4',
256 | 5: '5',
257 | 6: '6',
258 | 7: '7',
259 | 8: '8',
260 | 9: '9',
261 | 10: '10',
262 | 11: '11',
263 | 12: '12',
264 | 13: '13',
265 | },
266 | gridColumnStart: {
267 | auto: 'auto',
268 | 1: '1',
269 | 2: '2',
270 | 3: '3',
271 | 4: '4',
272 | 5: '5',
273 | 6: '6',
274 | 7: '7',
275 | 8: '8',
276 | 9: '9',
277 | 10: '10',
278 | 11: '11',
279 | 12: '12',
280 | 13: '13',
281 | },
282 | gridRow: {
283 | auto: 'auto',
284 | 'span-1': 'span 1 / span 1',
285 | 'span-2': 'span 2 / span 2',
286 | 'span-3': 'span 3 / span 3',
287 | 'span-4': 'span 4 / span 4',
288 | 'span-5': 'span 5 / span 5',
289 | 'span-6': 'span 6 / span 6',
290 | 'span-full': '1 / -1',
291 | },
292 | gridRowStart: {
293 | auto: 'auto',
294 | 1: '1',
295 | 2: '2',
296 | 3: '3',
297 | 4: '4',
298 | 5: '5',
299 | 6: '6',
300 | 7: '7',
301 | },
302 | gridRowEnd: {
303 | auto: 'auto',
304 | 1: '1',
305 | 2: '2',
306 | 3: '3',
307 | 4: '4',
308 | 5: '5',
309 | 6: '6',
310 | 7: '7',
311 | },
312 | gridTemplateColumns: {
313 | none: 'none',
314 | 1: 'repeat(1, minmax(0, 1fr))',
315 | 2: 'repeat(2, minmax(0, 1fr))',
316 | 3: 'repeat(3, minmax(0, 1fr))',
317 | 4: 'repeat(4, minmax(0, 1fr))',
318 | 5: 'repeat(5, minmax(0, 1fr))',
319 | 6: 'repeat(6, minmax(0, 1fr))',
320 | 7: 'repeat(7, minmax(0, 1fr))',
321 | 8: 'repeat(8, minmax(0, 1fr))',
322 | 9: 'repeat(9, minmax(0, 1fr))',
323 | 10: 'repeat(10, minmax(0, 1fr))',
324 | 11: 'repeat(11, minmax(0, 1fr))',
325 | 12: 'repeat(12, minmax(0, 1fr))',
326 | },
327 | gridTemplateRows: {
328 | none: 'none',
329 | 1: 'repeat(1, minmax(0, 1fr))',
330 | 2: 'repeat(2, minmax(0, 1fr))',
331 | 3: 'repeat(3, minmax(0, 1fr))',
332 | 4: 'repeat(4, minmax(0, 1fr))',
333 | 5: 'repeat(5, minmax(0, 1fr))',
334 | 6: 'repeat(6, minmax(0, 1fr))',
335 | },
336 | height: (theme) => ({
337 | auto: 'auto',
338 | ...theme('spacing'),
339 | '1/2': '50%',
340 | '1/3': '33.333333%',
341 | '2/3': '66.666667%',
342 | '1/4': '25%',
343 | '2/4': '50%',
344 | '3/4': '75%',
345 | '1/5': '20%',
346 | '2/5': '40%',
347 | '3/5': '60%',
348 | '4/5': '80%',
349 | '1/6': '16.666667%',
350 | '2/6': '33.333333%',
351 | '3/6': '50%',
352 | '4/6': '66.666667%',
353 | '5/6': '83.333333%',
354 | full: '100%',
355 | screen: '100vh',
356 | }),
357 | inset: (theme, { negative }) => ({
358 | auto: 'auto',
359 | ...theme('spacing'),
360 | ...negative(theme('spacing')),
361 | '1/2': '50%',
362 | '1/3': '33.333333%',
363 | '2/3': '66.666667%',
364 | '1/4': '25%',
365 | '2/4': '50%',
366 | '3/4': '75%',
367 | full: '100%',
368 | '-1/2': '-50%',
369 | '-1/3': '-33.333333%',
370 | '-2/3': '-66.666667%',
371 | '-1/4': '-25%',
372 | '-2/4': '-50%',
373 | '-3/4': '-75%',
374 | '-full': '-100%',
375 | }),
376 | keyframes: {
377 | spin: {
378 | to: {
379 | transform: 'rotate(360deg)',
380 | },
381 | },
382 | ping: {
383 | '75%, 100%': {
384 | transform: 'scale(2)',
385 | opacity: '0',
386 | },
387 | },
388 | pulse: {
389 | '50%': {
390 | opacity: '.5',
391 | },
392 | },
393 | bounce: {
394 | '0%, 100%': {
395 | transform: 'translateY(-25%)',
396 | animationTimingFunction: 'cubic-bezier(0.8,0,1,1)',
397 | },
398 | '50%': {
399 | transform: 'none',
400 | animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
401 | },
402 | },
403 | },
404 | letterSpacing: {
405 | tighter: '-0.05em',
406 | tight: '-0.025em',
407 | normal: '0em',
408 | wide: '0.025em',
409 | wider: '0.05em',
410 | widest: '0.1em',
411 | },
412 | lineHeight: {
413 | none: '1',
414 | tight: '1.25',
415 | snug: '1.375',
416 | normal: '1.5',
417 | relaxed: '1.625',
418 | loose: '2',
419 | 3: '.75rem',
420 | 4: '1rem',
421 | 5: '1.25rem',
422 | 6: '1.5rem',
423 | 7: '1.75rem',
424 | 8: '2rem',
425 | 9: '2.25rem',
426 | 10: '2.5rem',
427 | },
428 | listStyleType: {
429 | none: 'none',
430 | disc: 'disc',
431 | decimal: 'decimal',
432 | },
433 | margin: (theme, { negative }) => ({
434 | auto: 'auto',
435 | ...theme('spacing'),
436 | ...negative(theme('spacing')),
437 | }),
438 | maxHeight: (theme) => ({
439 | ...theme('spacing'),
440 | full: '100%',
441 | screen: '100vh',
442 | }),
443 | maxWidth: (theme, { breakpoints }) => ({
444 | none: 'none',
445 | 0: '0rem',
446 | xs: '20rem',
447 | sm: '24rem',
448 | md: '28rem',
449 | lg: '32rem',
450 | xl: '36rem',
451 | '2xl': '42rem',
452 | '3xl': '48rem',
453 | '4xl': '56rem',
454 | '5xl': '64rem',
455 | '6xl': '72rem',
456 | '7xl': '80rem',
457 | full: '100%',
458 | min: 'min-content',
459 | max: 'max-content',
460 | prose: '65ch',
461 | ...breakpoints(theme('screens')),
462 | }),
463 | minHeight: {
464 | 0: '0px',
465 | full: '100%',
466 | screen: '100vh',
467 | },
468 | minWidth: {
469 | 0: '0px',
470 | full: '100%',
471 | min: 'min-content',
472 | max: 'max-content',
473 | },
474 | objectPosition: {
475 | bottom: 'bottom',
476 | center: 'center',
477 | left: 'left',
478 | 'left-bottom': 'left bottom',
479 | 'left-top': 'left top',
480 | right: 'right',
481 | 'right-bottom': 'right bottom',
482 | 'right-top': 'right top',
483 | top: 'top',
484 | },
485 | opacity: {
486 | 0: '0',
487 | 5: '0.05',
488 | 10: '0.1',
489 | 20: '0.2',
490 | 25: '0.25',
491 | 30: '0.3',
492 | 40: '0.4',
493 | 50: '0.5',
494 | 60: '0.6',
495 | 70: '0.7',
496 | 75: '0.75',
497 | 80: '0.8',
498 | 90: '0.9',
499 | 95: '0.95',
500 | 100: '1',
501 | },
502 | order: {
503 | first: '-9999',
504 | last: '9999',
505 | none: '0',
506 | 1: '1',
507 | 2: '2',
508 | 3: '3',
509 | 4: '4',
510 | 5: '5',
511 | 6: '6',
512 | 7: '7',
513 | 8: '8',
514 | 9: '9',
515 | 10: '10',
516 | 11: '11',
517 | 12: '12',
518 | },
519 | outline: {
520 | none: ['2px solid transparent', '2px'],
521 | white: ['2px dotted white', '2px'],
522 | black: ['2px dotted black', '2px'],
523 | },
524 | padding: (theme) => theme('spacing'),
525 | placeholderColor: (theme) => theme('colors'),
526 | placeholderOpacity: (theme) => theme('opacity'),
527 | ringColor: (theme) => ({
528 | DEFAULT: theme('colors.blue.500', '#3b82f6'),
529 | ...theme('colors'),
530 | }),
531 | ringOffsetColor: (theme) => theme('colors'),
532 | ringOffsetWidth: {
533 | 0: '0px',
534 | 1: '1px',
535 | 2: '2px',
536 | 4: '4px',
537 | 8: '8px',
538 | },
539 | ringOpacity: (theme) => ({
540 | DEFAULT: '0.5',
541 | ...theme('opacity'),
542 | }),
543 | ringWidth: {
544 | DEFAULT: '3px',
545 | 0: '0px',
546 | 1: '1px',
547 | 2: '2px',
548 | 4: '4px',
549 | 8: '8px',
550 | },
551 | rotate: {
552 | '-180': '-180deg',
553 | '-90': '-90deg',
554 | '-45': '-45deg',
555 | '-12': '-12deg',
556 | '-6': '-6deg',
557 | '-3': '-3deg',
558 | '-2': '-2deg',
559 | '-1': '-1deg',
560 | 0: '0deg',
561 | 1: '1deg',
562 | 2: '2deg',
563 | 3: '3deg',
564 | 6: '6deg',
565 | 12: '12deg',
566 | 45: '45deg',
567 | 90: '90deg',
568 | 180: '180deg',
569 | },
570 | scale: {
571 | 0: '0',
572 | 50: '.5',
573 | 75: '.75',
574 | 90: '.9',
575 | 95: '.95',
576 | 100: '1',
577 | 105: '1.05',
578 | 110: '1.1',
579 | 125: '1.25',
580 | 150: '1.5',
581 | },
582 | skew: {
583 | '-12': '-12deg',
584 | '-6': '-6deg',
585 | '-3': '-3deg',
586 | '-2': '-2deg',
587 | '-1': '-1deg',
588 | 0: '0deg',
589 | 1: '1deg',
590 | 2: '2deg',
591 | 3: '3deg',
592 | 6: '6deg',
593 | 12: '12deg',
594 | },
595 | space: (theme, { negative }) => ({
596 | ...theme('spacing'),
597 | ...negative(theme('spacing')),
598 | }),
599 | stroke: {
600 | current: 'currentColor',
601 | },
602 | strokeWidth: {
603 | 0: '0',
604 | 1: '1',
605 | 2: '2',
606 | },
607 | textColor: (theme) => theme('colors'),
608 | textOpacity: (theme) => theme('opacity'),
609 | transformOrigin: {
610 | center: 'center',
611 | top: 'top',
612 | 'top-right': 'top right',
613 | right: 'right',
614 | 'bottom-right': 'bottom right',
615 | bottom: 'bottom',
616 | 'bottom-left': 'bottom left',
617 | left: 'left',
618 | 'top-left': 'top left',
619 | },
620 | transitionDelay: {
621 | 75: '75ms',
622 | 100: '100ms',
623 | 150: '150ms',
624 | 200: '200ms',
625 | 300: '300ms',
626 | 500: '500ms',
627 | 700: '700ms',
628 | 1000: '1000ms',
629 | },
630 | transitionDuration: {
631 | DEFAULT: '150ms',
632 | 75: '75ms',
633 | 100: '100ms',
634 | 150: '150ms',
635 | 200: '200ms',
636 | 300: '300ms',
637 | 500: '500ms',
638 | 700: '700ms',
639 | 1000: '1000ms',
640 | },
641 | transitionProperty: {
642 | none: 'none',
643 | all: 'all',
644 | DEFAULT: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
645 | colors: 'background-color, border-color, color, fill, stroke',
646 | opacity: 'opacity',
647 | shadow: 'box-shadow',
648 | transform: 'transform',
649 | },
650 | transitionTimingFunction: {
651 | DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)',
652 | linear: 'linear',
653 | in: 'cubic-bezier(0.4, 0, 1, 1)',
654 | out: 'cubic-bezier(0, 0, 0.2, 1)',
655 | 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
656 | },
657 | translate: (theme, { negative }) => ({
658 | ...theme('spacing'),
659 | ...negative(theme('spacing')),
660 | '1/2': '50%',
661 | '1/3': '33.333333%',
662 | '2/3': '66.666667%',
663 | '1/4': '25%',
664 | '2/4': '50%',
665 | '3/4': '75%',
666 | full: '100%',
667 | '-1/2': '-50%',
668 | '-1/3': '-33.333333%',
669 | '-2/3': '-66.666667%',
670 | '-1/4': '-25%',
671 | '-2/4': '-50%',
672 | '-3/4': '-75%',
673 | '-full': '-100%',
674 | }),
675 | width: (theme) => ({
676 | auto: 'auto',
677 | ...theme('spacing'),
678 | '1/2': '50%',
679 | '1/3': '33.333333%',
680 | '2/3': '66.666667%',
681 | '1/4': '25%',
682 | '2/4': '50%',
683 | '3/4': '75%',
684 | '1/5': '20%',
685 | '2/5': '40%',
686 | '3/5': '60%',
687 | '4/5': '80%',
688 | '1/6': '16.666667%',
689 | '2/6': '33.333333%',
690 | '3/6': '50%',
691 | '4/6': '66.666667%',
692 | '5/6': '83.333333%',
693 | '1/12': '8.333333%',
694 | '2/12': '16.666667%',
695 | '3/12': '25%',
696 | '4/12': '33.333333%',
697 | '5/12': '41.666667%',
698 | '6/12': '50%',
699 | '7/12': '58.333333%',
700 | '8/12': '66.666667%',
701 | '9/12': '75%',
702 | '10/12': '83.333333%',
703 | '11/12': '91.666667%',
704 | full: '100%',
705 | screen: '100vw',
706 | min: 'min-content',
707 | max: 'max-content',
708 | }),
709 | zIndex: {
710 | auto: 'auto',
711 | 0: '0',
712 | 10: '10',
713 | 20: '20',
714 | 30: '30',
715 | 40: '40',
716 | 50: '50',
717 | },
718 | },
719 | variantOrder: [
720 | 'first',
721 | 'last',
722 | 'odd',
723 | 'even',
724 | 'visited',
725 | 'checked',
726 | 'group-hover',
727 | 'group-focus',
728 | 'focus-within',
729 | 'hover',
730 | 'focus',
731 | 'focus-visible',
732 | 'active',
733 | 'disabled',
734 | ],
735 | variants: {
736 | accessibility: ['responsive', 'focus-within', 'focus'],
737 | alignContent: ['responsive'],
738 | alignItems: ['responsive'],
739 | alignSelf: ['responsive'],
740 | animation: ['responsive'],
741 | appearance: ['responsive'],
742 | backgroundAttachment: ['responsive'],
743 | backgroundClip: ['responsive'],
744 | backgroundColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
745 | backgroundImage: ['responsive'],
746 | backgroundOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
747 | backgroundPosition: ['responsive'],
748 | backgroundRepeat: ['responsive'],
749 | backgroundSize: ['responsive'],
750 | borderCollapse: ['responsive'],
751 | borderColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
752 | borderOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
753 | borderRadius: ['responsive'],
754 | borderStyle: ['responsive'],
755 | borderWidth: ['responsive'],
756 | boxShadow: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
757 | boxSizing: ['responsive'],
758 | clear: ['responsive'],
759 | container: ['responsive'],
760 | cursor: ['responsive'],
761 | display: ['responsive'],
762 | divideColor: ['responsive', 'dark'],
763 | divideOpacity: ['responsive', 'dark'],
764 | divideStyle: ['responsive'],
765 | divideWidth: ['responsive'],
766 | fill: ['responsive'],
767 | flex: ['responsive'],
768 | flexDirection: ['responsive'],
769 | flexGrow: ['responsive'],
770 | flexShrink: ['responsive'],
771 | flexWrap: ['responsive'],
772 | float: ['responsive'],
773 | fontFamily: ['responsive'],
774 | fontSize: ['responsive'],
775 | fontSmoothing: ['responsive'],
776 | fontStyle: ['responsive'],
777 | fontVariantNumeric: ['responsive'],
778 | fontWeight: ['responsive'],
779 | gap: ['responsive'],
780 | gradientColorStops: ['responsive', 'dark', 'hover', 'focus'],
781 | gridAutoColumns: ['responsive'],
782 | gridAutoFlow: ['responsive'],
783 | gridAutoRows: ['responsive'],
784 | gridColumn: ['responsive'],
785 | gridColumnEnd: ['responsive'],
786 | gridColumnStart: ['responsive'],
787 | gridRow: ['responsive'],
788 | gridRowEnd: ['responsive'],
789 | gridRowStart: ['responsive'],
790 | gridTemplateColumns: ['responsive'],
791 | gridTemplateRows: ['responsive'],
792 | height: ['responsive'],
793 | inset: ['responsive'],
794 | justifyContent: ['responsive'],
795 | justifyItems: ['responsive'],
796 | justifySelf: ['responsive'],
797 | letterSpacing: ['responsive'],
798 | lineHeight: ['responsive'],
799 | listStylePosition: ['responsive'],
800 | listStyleType: ['responsive'],
801 | margin: ['responsive'],
802 | maxHeight: ['responsive'],
803 | maxWidth: ['responsive'],
804 | minHeight: ['responsive'],
805 | minWidth: ['responsive'],
806 | objectFit: ['responsive'],
807 | objectPosition: ['responsive'],
808 | opacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
809 | order: ['responsive'],
810 | outline: ['responsive', 'focus-within', 'focus'],
811 | overflow: ['responsive'],
812 | overscrollBehavior: ['responsive'],
813 | padding: ['responsive'],
814 | placeContent: ['responsive'],
815 | placeItems: ['responsive'],
816 | placeSelf: ['responsive'],
817 | placeholderColor: ['responsive', 'dark', 'focus'],
818 | placeholderOpacity: ['responsive', 'dark', 'focus'],
819 | pointerEvents: ['responsive'],
820 | position: ['responsive'],
821 | resize: ['responsive'],
822 | ringColor: ['responsive', 'dark', 'focus-within', 'focus'],
823 | ringOffsetColor: ['responsive', 'dark', 'focus-within', 'focus'],
824 | ringOffsetWidth: ['responsive', 'focus-within', 'focus'],
825 | ringOpacity: ['responsive', 'dark', 'focus-within', 'focus'],
826 | ringWidth: ['responsive', 'focus-within', 'focus'],
827 | rotate: ['responsive', 'hover', 'focus'],
828 | scale: ['responsive', 'hover', 'focus'],
829 | skew: ['responsive', 'hover', 'focus'],
830 | space: ['responsive'],
831 | stroke: ['responsive'],
832 | strokeWidth: ['responsive'],
833 | tableLayout: ['responsive'],
834 | textAlign: ['responsive'],
835 | textColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
836 | textDecoration: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
837 | textOpacity: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
838 | textOverflow: ['responsive'],
839 | textTransform: ['responsive'],
840 | transform: ['responsive'],
841 | transformOrigin: ['responsive'],
842 | transitionDelay: ['responsive'],
843 | transitionDuration: ['responsive'],
844 | transitionProperty: ['responsive'],
845 | transitionTimingFunction: ['responsive'],
846 | translate: ['responsive', 'hover', 'focus'],
847 | userSelect: ['responsive'],
848 | verticalAlign: ['responsive'],
849 | visibility: ['responsive'],
850 | whitespace: ['responsive'],
851 | width: ['responsive'],
852 | wordBreak: ['responsive'],
853 | zIndex: ['responsive', 'focus-within', 'focus'],
854 | },
855 | plugins: [],
856 | }
857 |
--------------------------------------------------------------------------------