mirror of
https://github.com/chenasraf/dotfiles.git
synced 2026-05-17 17:28:07 +00:00
add more templates
This commit is contained in:
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
templates/
|
||||
scaffolds/
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
templates/
|
||||
scaffolds/
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"dotfiles"
|
||||
],
|
||||
"python.linting.enabled": true
|
||||
"cSpell.words": [
|
||||
"dotfiles"
|
||||
],
|
||||
"python.linting.enabled": true
|
||||
}
|
||||
|
||||
7
.zshrc
7
.zshrc
@@ -3,5 +3,8 @@ source $HOME/.dotfiles/zsh_init.sh
|
||||
source $HOME/.dotfiles/aliases.sh
|
||||
source $HOME/.dotfiles/sources.sh
|
||||
source $HOME/.dotfiles/home.sh
|
||||
source $HOME/.dotfiles/scripts/java.sh
|
||||
source $HOME/.dotfiles/scripts/zi.sh
|
||||
|
||||
# source all files in scripts dir
|
||||
for file in $HOME/.dotfiles/scripts/*.sh; do
|
||||
source $file
|
||||
done
|
||||
|
||||
@@ -29,7 +29,6 @@ alias dgi_gen="$GOBIN/gi_gen"
|
||||
alias ggi_gen="$DOTBIN/gi_gen"
|
||||
# go [i]nstall & run gi_gen
|
||||
alias igi_gen="go install && dgi_gen"
|
||||
alias tsfiles="yes | npx simple-scaffold@latest -t '$DOTFILES/scaffolds/tsfiles' -o . -"
|
||||
|
||||
# Functions
|
||||
mansect() { man -aWS ${1?man section not provided} \* | xargs basename | sed "s/\.[^.]*$//" | sort -u; }
|
||||
|
||||
8
scaffolds/nextjs/.editorconfig
Normal file
8
scaffolds/nextjs/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[*]
|
||||
tab_width = 2
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
1
scaffolds/nextjs/.eslintignore
Normal file
1
scaffolds/nextjs/.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
templates/**/*
|
||||
9
scaffolds/nextjs/.eslintrc.js
Normal file
9
scaffolds/nextjs/.eslintrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
extends: ['next/core-web-vitals', 'plugin:@typescript-eslint/recommended'],
|
||||
rules: {
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
}
|
||||
38
scaffolds/nextjs/.gitignore
vendored
Normal file
38
scaffolds/nextjs/.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# swagger definitions
|
||||
/swagger.*
|
||||
15
scaffolds/nextjs/.prettierrc
Normal file
15
scaffolds/nextjs/.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
scaffolds/nextjs/.vscode/settings.json
vendored
Normal file
16
scaffolds/nextjs/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"{{ hyphenCase name }}",
|
||||
"MAPBOX",
|
||||
"mapboxgl",
|
||||
"stylis",
|
||||
"Unmatch"
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"public/locales"
|
||||
],
|
||||
"i18n-ally.sourceLanguage": "he",
|
||||
"i18n-ally.displayLanguage": "he",
|
||||
"i18next.i18nPaths": "/Users/chen/Dev/nestify-web/public/locales",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
}
|
||||
30
scaffolds/nextjs/.vscode/snippets.code-snippets
vendored
Normal file
30
scaffolds/nextjs/.vscode/snippets.code-snippets
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
// Place your {{ hyphenCase name }}-web workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"Load Translation SSR Props": {
|
||||
"scope": "javascriptreact,typescriptreact",
|
||||
"prefix": "tssr",
|
||||
"body": [
|
||||
"export async function getStaticProps({ locale }: NextPageContext) {",
|
||||
" return {",
|
||||
" props: await getI18nProps(locale ?? 'he', ['${1:{{ hyphenCase name }}}']),",
|
||||
" }",
|
||||
"}",
|
||||
],
|
||||
"description": "Load Translation SSR Props",
|
||||
}
|
||||
}
|
||||
20
scaffolds/nextjs/.vscode/tasks.json
vendored
Normal file
20
scaffolds/nextjs/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: dev",
|
||||
"detail": "next dev"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: build",
|
||||
"detail": "next build"
|
||||
},
|
||||
]
|
||||
}
|
||||
57
scaffolds/nextjs/Dockerfile
Normal file
57
scaffolds/nextjs/Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# You only need to copy next.config.js if you are NOT using the default configuration
|
||||
COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
53
scaffolds/nextjs/README.md
Normal file
53
scaffolds/nextjs/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# {{ pascalCase name }} Web App
|
||||
|
||||
## Dev Requirements
|
||||
|
||||
1. [Node.js](https://nodejs.org/en/download/)
|
||||
2. Yarn: `npm install -g yarn`
|
||||
|
||||
> These requirements are for development mode only.
|
||||
> You can run this project statically using Docker without having to install other dependencies.
|
||||
|
||||
## Run in dev mode
|
||||
|
||||
Dev mode allows hot reloading of files & components, and JIT build; as opposed to AOT build +
|
||||
static serving.
|
||||
|
||||
1. Install/update project dependencies: `yarn install`
|
||||
2. Add `.env.local` file to the repository root folder, and fill it with the correct env variables:
|
||||
|
||||
```shell
|
||||
NEXT_PUBLIC_API_BASE=
|
||||
NEXT_PUBLIC_MAPBOX_API_KEY=
|
||||
NEXT_PUBLIC_FACEBOOK_APP_ID=
|
||||
# to use test social app login instead of prod one in backend
|
||||
# (optional, automatically true in dev mode)
|
||||
# NEXT_PUBLIC_API_TEST=true
|
||||
```
|
||||
|
||||
3. Run in development mode with hot reload: `yarn dev`
|
||||
4. Open [http://localhost:3000](http://localhost:3000) with your browser
|
||||
|
||||
## Run using Docker
|
||||
|
||||
Add the `.env.local` file mentioned in "Run in dev mode" to build the Docker with the correct
|
||||
environment. Then, you can build the container and run as an image:
|
||||
|
||||
### Shell - Manual
|
||||
|
||||
```shell
|
||||
# build
|
||||
docker build -t nextjs-docker .
|
||||
# run
|
||||
# this docker exposes port 3000, you may forward to any other port
|
||||
# (in this case 3100, to avoid conflict with dev mode run)
|
||||
docker run -p 3100:3000 nextjs-docker
|
||||
```
|
||||
|
||||
### NPM Script - Automated
|
||||
|
||||
```shell
|
||||
# build & run via npm scripts if you have Node.js installed
|
||||
yarn build:docker
|
||||
yarn start:docker
|
||||
```
|
||||
47
scaffolds/nextjs/components/atoms/Avatar.tsx
Normal file
47
scaffolds/nextjs/components/atoms/Avatar.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import Box, { BoxProps } from '@mui/material/Box'
|
||||
import { sxc } from '../../core/utils/object_utils'
|
||||
import { CustomComponent } from '../../core/types'
|
||||
import Image from 'next/image'
|
||||
import { apiFallbackImageUrl } from '../../core/utils/image_utils'
|
||||
|
||||
export interface AvatarProps extends CustomComponent<BoxProps> {
|
||||
src: string
|
||||
size?: number
|
||||
padding?: number
|
||||
}
|
||||
|
||||
export const Avatar: React.FC<AvatarProps> = ({ sx, size = 160, src, padding = 8 }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={sxc(
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
boxShadow: (theme) => theme.boxShadows.avatar,
|
||||
padding: `${padding}px`,
|
||||
},
|
||||
sx,
|
||||
)}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: size - padding * 2,
|
||||
height: size - padding * 2,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={apiFallbackImageUrl(src)}
|
||||
objectFit="cover"
|
||||
width={size}
|
||||
height={size}
|
||||
alt={src}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
76
scaffolds/nextjs/components/atoms/ControlledAutocomplete.tsx
Normal file
76
scaffolds/nextjs/components/atoms/ControlledAutocomplete.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import React from 'react'
|
||||
import { Controller, Control, Path } from 'react-hook-form'
|
||||
import { CustomComponent, Option } from '../../core/types'
|
||||
import { optionFrom } from '../../core/utils/object_utils'
|
||||
|
||||
export interface ControlledAutocompleteProps<T, K extends Path<T>, Multiple extends boolean = false>
|
||||
extends CustomComponent<Partial<AutocompleteProps<Option, Multiple, false, false>>> {
|
||||
control: Control<T>
|
||||
name: K
|
||||
options: Option[]
|
||||
helperText?: React.ReactNode
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
multiple?: Multiple
|
||||
}
|
||||
|
||||
export const ControlledAutocomplete = <T, K extends Path<T>, Multiple extends boolean = false>(
|
||||
props: ControlledAutocompleteProps<T, K, Multiple>,
|
||||
) => {
|
||||
const { control, name, options, helperText, placeholder, onChange, multiple, ...rest } = props
|
||||
|
||||
const _value = React.useMemo(
|
||||
(): Multiple extends true ? Option<string>[] : Option<string> | null =>
|
||||
control._formValues[name]
|
||||
? multiple
|
||||
? control._formValues[name].map(optionFrom)
|
||||
: optionFrom(control._formValues[name])
|
||||
: multiple
|
||||
? []
|
||||
: null,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[control._formValues[name]],
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [value, setValue] = React.useState(_value ?? (multiple ? ([] as string[]) : ('' as any)))
|
||||
React.useEffect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
() => setValue(_value ?? (multiple ? ([] as string[]) : ('' as any))),
|
||||
[_value, multiple],
|
||||
)
|
||||
|
||||
return (
|
||||
<Controller<T, K>
|
||||
control={control}
|
||||
name={name}
|
||||
rules={{ required: props.required }}
|
||||
render={({ field, formState: { errors } }) => (
|
||||
<Autocomplete
|
||||
onChange={(e, d, r) => {
|
||||
setValue(d as typeof _value)
|
||||
field.onChange(Array.isArray(d) ? d.map((d) => d?.value) : d?.value)
|
||||
onChange?.(e, d, r)
|
||||
}}
|
||||
value={value}
|
||||
options={options}
|
||||
getOptionLabel={(opt) => opt?.label}
|
||||
isOptionEqualToValue={(opt, value) => opt.value === value.value}
|
||||
multiple={multiple}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
placeholder={placeholder}
|
||||
helperText={helperText}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error={Boolean((errors as any)[name])}
|
||||
/>
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
117
scaffolds/nextjs/components/atoms/ControlledSelect.tsx
Normal file
117
scaffolds/nextjs/components/atoms/ControlledSelect.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import Select, { SelectProps } from '@mui/material/Select'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import React from 'react'
|
||||
import { Controller, Control, Path } from 'react-hook-form'
|
||||
import { CustomComponent, Option } from '../../core/types'
|
||||
import { useRerender } from '../../core/utils/react_utils'
|
||||
|
||||
export interface ControlledSelectProps<
|
||||
R extends string,
|
||||
T,
|
||||
K extends Path<T>,
|
||||
Multiple extends boolean = false,
|
||||
> extends CustomComponent<Partial<SelectProps<R>>> {
|
||||
control: Control<T>
|
||||
name: K
|
||||
options: Option<R>[]
|
||||
helperText?: React.ReactNode
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
multiple?: Multiple
|
||||
}
|
||||
|
||||
export const ControlledSelect = <
|
||||
R extends string,
|
||||
T,
|
||||
K extends Path<T>,
|
||||
Multiple extends boolean = false,
|
||||
>(
|
||||
props: ControlledSelectProps<R, T, K, Multiple>,
|
||||
) => {
|
||||
const {
|
||||
control,
|
||||
name,
|
||||
options,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
helperText,
|
||||
placeholder,
|
||||
onChange,
|
||||
multiple = false,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const _value = React.useMemo(
|
||||
(): typeof multiple extends true ? R[] : R | null =>
|
||||
(control._formValues[name]
|
||||
? multiple
|
||||
? (control._formValues[name] as R[]).map((x) => options.find((y) => y.value === x)!.value)
|
||||
: options.find((x) => x.value === control._formValues[name])!.value
|
||||
: multiple
|
||||
? []
|
||||
: null) as typeof multiple extends true ? R[] : R | null,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[control._formValues[name], multiple, name, options],
|
||||
)
|
||||
const [value, setValue] = React.useState<string | string[]>(
|
||||
(Array.isArray(_value) ? _value : _value) ?? (multiple ? [] : ''),
|
||||
)
|
||||
React.useEffect(() => setValue(_value ?? (multiple ? [] : '')), [_value, multiple])
|
||||
const rerender = useRerender()
|
||||
|
||||
return (
|
||||
<Controller<T, K>
|
||||
control={control}
|
||||
name={name}
|
||||
rules={{ required: props.required }}
|
||||
render={({ field, formState: { errors } }) => (
|
||||
<Select
|
||||
onChange={(e, d) => {
|
||||
const { value: changeValue } = e.target!
|
||||
if (!multiple) {
|
||||
field.onChange(changeValue)
|
||||
setValue(changeValue)
|
||||
} else {
|
||||
const valueArr = value as R[]
|
||||
const fieldValue = valueArr.includes(changeValue as unknown as R)
|
||||
? valueArr.filter((x) => x !== changeValue)
|
||||
: [...valueArr, changeValue]
|
||||
field.onChange(fieldValue)
|
||||
setValue(fieldValue)
|
||||
}
|
||||
|
||||
onChange?.(e, d)
|
||||
rerender()
|
||||
}}
|
||||
value={value as R}
|
||||
renderValue={
|
||||
((v) =>
|
||||
Array.isArray(v)
|
||||
? placeholder
|
||||
: options.find((x) => x.value === v)?.label ?? placeholder) as (
|
||||
value: string | string[],
|
||||
) => React.ReactNode
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error={Boolean((errors as any)[name])}
|
||||
{...rest}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<MenuItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
sx={
|
||||
Boolean(Array.isArray(value) ? value.includes(opt.value) : value === opt.value)
|
||||
? {
|
||||
backgroundColor: (theme) => theme.palette.grey[200],
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
30
scaffolds/nextjs/components/atoms/PageLoader.tsx
Normal file
30
scaffolds/nextjs/components/atoms/PageLoader.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import Box, { BoxProps } from '@mui/material/Box'
|
||||
import { CustomComponent } from '../../core/types'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import { TOOLBAR_HEIGHT } from '../layout/MainAppBar'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PageLoaderProps extends CustomComponent<BoxProps> {
|
||||
//
|
||||
}
|
||||
|
||||
export const PageLoader: React.FC<PageLoaderProps> = ({ sx }) => {
|
||||
return (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={TOOLBAR_HEIGHT}
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="100%"
|
||||
height="calc(100vh - 90px)"
|
||||
sx={sx}
|
||||
>
|
||||
<CircularProgress size={64} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
62
scaffolds/nextjs/components/atoms/PageWrapper.tsx
Normal file
62
scaffolds/nextjs/components/atoms/PageWrapper.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react'
|
||||
import Box, { BoxProps } from '@mui/material/Box'
|
||||
import { sxc } from '../../core/utils/object_utils'
|
||||
import { CustomComponent } from '../../core/types'
|
||||
import { MainAppBar, TOOLBAR_HEIGHT } from '../layout/MainAppBar'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import { PageLoader } from './PageLoader'
|
||||
import Head from 'next/head'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
export interface PageWrapperProps extends CustomComponent<BoxProps> {
|
||||
fixedMaxWidth?: boolean
|
||||
loading?: boolean
|
||||
pageTitle?: React.ReactNode
|
||||
htmlTitle?: string
|
||||
htmlDescription?: string
|
||||
render?: (props: PageWrapperProps) => React.ReactNode
|
||||
}
|
||||
|
||||
export const PageWrapper: React.FC<PageWrapperProps> = (props) => {
|
||||
const {
|
||||
sx,
|
||||
children,
|
||||
loading,
|
||||
fixedMaxWidth = true,
|
||||
htmlTitle,
|
||||
htmlDescription,
|
||||
pageTitle,
|
||||
render,
|
||||
} = props
|
||||
return (
|
||||
<Box
|
||||
sx={sxc(
|
||||
{
|
||||
padding: (theme) => theme.spacing(3, 2),
|
||||
margin: '0 auto',
|
||||
},
|
||||
fixedMaxWidth && {
|
||||
maxWidth: 600,
|
||||
},
|
||||
sx,
|
||||
)}
|
||||
>
|
||||
{htmlTitle || htmlDescription ? (
|
||||
<Head>
|
||||
<title>{htmlTitle}</title>
|
||||
<meta name="description" content={htmlDescription} />
|
||||
</Head>
|
||||
) : null}
|
||||
{/* App Bar */}
|
||||
<MainAppBar />
|
||||
{/* Empty toolbar - to save space for fixed app bar */}
|
||||
<Toolbar sx={{ height: `${TOOLBAR_HEIGHT}px` }} />
|
||||
{pageTitle ? (
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
{pageTitle}
|
||||
</Typography>
|
||||
) : null}
|
||||
{loading ? <PageLoader /> : render?.(props) ?? children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
54
scaffolds/nextjs/components/hooks/flat_hooks.ts
Normal file
54
scaffolds/nextjs/components/hooks/flat_hooks.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
import flatsApi from '../../core/api/flats.api'
|
||||
import { Flat } from '../../core/models/flats'
|
||||
import { createUseApi } from '../../core/utils/react_utils'
|
||||
|
||||
export const useMyFlat = createUseApi(() => flatsApi.getMyFlat(), {
|
||||
cacheKey: 'myFlat',
|
||||
responseKey: 'flat',
|
||||
})
|
||||
|
||||
export const usePotentialFlats = createUseApi(() => flatsApi.getPotentialFlatsList(), {
|
||||
cacheKey: 'potentialFlats',
|
||||
responseKey: 'potentialFlats',
|
||||
innerKey: 'flat',
|
||||
})
|
||||
|
||||
export const useMatchedFlats = createUseApi(() => flatsApi.getMatchedFlatsList(), {
|
||||
cacheKey: 'matchedFlats',
|
||||
responseKey: 'fullMatches',
|
||||
innerKey: 'matchesByUser',
|
||||
})
|
||||
|
||||
export const useLikedFlats = createUseApi(() => flatsApi.getLikedFlatsList(), {
|
||||
cacheKey: 'likedFlats',
|
||||
responseKey: 'likedFlats',
|
||||
innerKey: 'flat',
|
||||
})
|
||||
|
||||
export const useDislikedFlats = createUseApi(() => flatsApi.getDislikedFlatsList(), {
|
||||
cacheKey: 'dislikedFlats',
|
||||
responseKey: 'dislikedFlats',
|
||||
innerKey: 'flat',
|
||||
})
|
||||
|
||||
export function useFlatCache(list: Flat[] | undefined) {
|
||||
const [cache, setCache] = React.useState<Flat[]>(list ?? [])
|
||||
|
||||
React.useEffect(() => {
|
||||
setCache(list ?? [])
|
||||
}, [list])
|
||||
|
||||
return {
|
||||
data: cache,
|
||||
add: (item: Flat) => {
|
||||
setCache([...cache, item])
|
||||
},
|
||||
update: (item: Flat) => {
|
||||
setCache(cache.map((flat) => (flat.userId === item.userId ? item : flat)))
|
||||
},
|
||||
remove: (item: Flat) => {
|
||||
setCache(cache.filter((flat) => flat.userId !== item.userId))
|
||||
},
|
||||
}
|
||||
}
|
||||
17
scaffolds/nextjs/components/hooks/intl_hooks.ts
Normal file
17
scaffolds/nextjs/components/hooks/intl_hooks.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React from 'react'
|
||||
|
||||
export function useNumberFormatter(options?: Intl.NumberFormatOptions): Intl.NumberFormat {
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
return React.useMemo(() => {
|
||||
return Intl.NumberFormat(i18n.language, options)
|
||||
}, [i18n.language, options])
|
||||
}
|
||||
export function useCurrencyFormatter(options?: Intl.NumberFormatOptions): Intl.NumberFormat {
|
||||
return useNumberFormatter({
|
||||
style: 'currency',
|
||||
currency: 'ILS',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
10
scaffolds/nextjs/components/hooks/notification_hooks.ts
Normal file
10
scaffolds/nextjs/components/hooks/notification_hooks.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import notificationsApi from '../../core/api/notifications.api'
|
||||
import { Notification } from '../../core/models/notification'
|
||||
import { createUseApi } from '../../core/utils/react_utils'
|
||||
|
||||
const getId = (notification: Notification) => notification.userId
|
||||
|
||||
export const useNotifications = createUseApi(() => notificationsApi.getNotifications(), {
|
||||
cacheKey: 'notifications',
|
||||
responseKey: 'notifications',
|
||||
})
|
||||
21
scaffolds/nextjs/components/hooks/place_hooks.ts
Normal file
21
scaffolds/nextjs/components/hooks/place_hooks.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import placesApi, { GetPlacesListResult } from '../../core/api/places.api'
|
||||
import uniqBy from 'lodash/uniqBy'
|
||||
import React from 'react'
|
||||
import { createUseApi, RequiredQueryOptions } from '../../core/utils/react_utils'
|
||||
|
||||
export const usePlaces = createUseApi(() => placesApi.getPlacesList(), {
|
||||
cacheKey: 'places',
|
||||
responseKey: 'places',
|
||||
})
|
||||
|
||||
export function useCities(options?: RequiredQueryOptions<GetPlacesListResult>) {
|
||||
const { places = [], ...rest } = usePlaces(options)
|
||||
const data = React.useMemo(() => uniqBy(places, 'city'), [places])
|
||||
return { ...rest, cities: data }
|
||||
}
|
||||
|
||||
export function useNeighborhoods(options?: RequiredQueryOptions<GetPlacesListResult>) {
|
||||
const { places = [], ...rest } = usePlaces(options)
|
||||
const data = React.useMemo(() => uniqBy(places, 'neighborhood'), [places])
|
||||
return { ...rest, neighborhoods: data }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import userPreferencesApi from '../../core/api/user_preferences.api'
|
||||
import { createUseApi } from '../../core/utils/react_utils'
|
||||
|
||||
export const useUserPreferences = createUseApi(() => userPreferencesApi.getUserPreferences(), {
|
||||
cacheKey: 'userPreferences',
|
||||
responseKey: 'userPreference',
|
||||
})
|
||||
11
scaffolds/nextjs/components/layout/Link.tsx
Normal file
11
scaffolds/nextjs/components/layout/Link.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import NextLink, { LinkProps as NextLinkProps } from 'next/link'
|
||||
import MUILink, { LinkProps as MUILinkProps } from '@mui/material/Link'
|
||||
|
||||
export const Link: React.FC<MUILinkProps> = (props) => {
|
||||
const { href, children, ...rest } = props
|
||||
return (
|
||||
<NextLink passHref href={href!}>
|
||||
<MUILink {...rest}>{children}</MUILink>
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
64
scaffolds/nextjs/components/layout/MainAppBar.tsx
Normal file
64
scaffolds/nextjs/components/layout/MainAppBar.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import ChevronLeft from '@mui/icons-material/ChevronLeft'
|
||||
import MenuIcon from '@mui/icons-material/Menu'
|
||||
import Notifications from '@mui/icons-material/Notifications'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import AppBar from '@mui/material/AppBar'
|
||||
import React from 'react'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import Box from '@mui/material/Box'
|
||||
import { Routes, usePushRoute } from '../../core/routes'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
|
||||
export const TOOLBAR_HEIGHT = 68
|
||||
|
||||
export const MainAppBar: React.FC = React.memo(function MainAppBar() {
|
||||
const [menuRef, setMenuRef] = React.useState<HTMLButtonElement | null>(null)
|
||||
const goTo = usePushRoute()
|
||||
const { t } = useTranslation('{{ hyphenCase name }}')
|
||||
|
||||
const handleMenuClick = React.useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setMenuRef(e.currentTarget)
|
||||
}, [])
|
||||
|
||||
const handleMenuClose = React.useCallback(() => {
|
||||
setMenuRef(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar color="inherit">
|
||||
<Toolbar sx={{ height: `${TOOLBAR_HEIGHT}px` }}>
|
||||
<IconButton onClick={handleMenuClick} color="inherit">
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" onClick={() => goTo(Routes.Notifications)}>
|
||||
<Notifications />
|
||||
</IconButton>
|
||||
<Box display="flex" flexGrow={1} alignItems="center" justifyContent="center">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/logo.svg" alt="{{ pascalCase name }}" width={115} height={25} />
|
||||
</Box>
|
||||
<IconButton color="inherit">
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Menu anchorEl={menuRef} open={!!menuRef} onClose={handleMenuClose}>
|
||||
<MenuItem onClick={() => goTo(Routes.PotentialFlatsList)}>
|
||||
{t('app_bar.menu.results')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => goTo(Routes.MatchedFlatsList)}>
|
||||
{t('app_bar.menu.matches')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => goTo(Routes.LikedFlatsList)}>{t('app_bar.menu.liked')}</MenuItem>
|
||||
<MenuItem onClick={() => goTo(Routes.DislikedFlatsList)}>
|
||||
{t('app_bar.menu.disliked')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => goTo(Routes.Profile)}>{t('app_bar.menu.profile')}</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
})
|
||||
39
scaffolds/nextjs/core/api/api.ts
Normal file
39
scaffolds/nextjs/core/api/api.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import axios, { Axios, AxiosResponse } from 'axios'
|
||||
import { ENV } from '../env'
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: ENV.API_BASE + '/api/v1',
|
||||
headers: ENV.API_TEST ? { Test: 'true' } : undefined,
|
||||
})
|
||||
|
||||
export class ApiCollection {
|
||||
client!: Axios
|
||||
|
||||
constructor() {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
public setAuthHeaders(token: string) {
|
||||
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
public unsetAuthHeaders() {
|
||||
delete this.client.defaults.headers.common['Authorization']
|
||||
}
|
||||
|
||||
protected async parse<T, K extends keyof T>(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
result: Promise<AxiosResponse<T>>,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
key: K,
|
||||
): Promise<T[K]>
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
protected async parse<T, K extends keyof T>(result: Promise<AxiosResponse<T>>): Promise<T>
|
||||
protected async parse<T, K extends keyof T>(
|
||||
result: Promise<AxiosResponse<T>>,
|
||||
key?: K,
|
||||
): Promise<T | T[K]> {
|
||||
const { data } = await result
|
||||
return key ? data[key] : data
|
||||
}
|
||||
}
|
||||
13
scaffolds/nextjs/core/api/api_types.ts
Normal file
13
scaffolds/nextjs/core/api/api_types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type SingleResult<T, K extends string> = {
|
||||
[result in K]: T
|
||||
} & {
|
||||
error: boolean
|
||||
msg: string
|
||||
}
|
||||
export type ListResult<T, K extends string> = {
|
||||
[result in K]?: T[]
|
||||
} & {
|
||||
count: number
|
||||
error: boolean
|
||||
msg: string
|
||||
}
|
||||
1003
scaffolds/nextjs/core/data/cities.json
Normal file
1003
scaffolds/nextjs/core/data/cities.json
Normal file
File diff suppressed because it is too large
Load Diff
11
scaffolds/nextjs/core/emotion.ts
Normal file
11
scaffolds/nextjs/core/emotion.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import createCache from '@emotion/cache'
|
||||
import stylisRTLPlugin from 'stylis-plugin-rtl'
|
||||
import { prefixer } from 'stylis'
|
||||
|
||||
export const createEmotionCache = () => {
|
||||
return createCache({
|
||||
key: 'css',
|
||||
prepend: true,
|
||||
stylisPlugins: [prefixer, stylisRTLPlugin],
|
||||
})
|
||||
}
|
||||
7
scaffolds/nextjs/core/env.ts
Normal file
7
scaffolds/nextjs/core/env.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const ENV = {
|
||||
BROWSER_LOADED: typeof window !== 'undefined',
|
||||
API_BASE: process.env.NEXT_PUBLIC_API_BASE!,
|
||||
API_TEST: process.env.NODE_ENV === 'development' || process.env.NEXT_PUBLIC_API_TEST === 'true',
|
||||
MAPBOX_API_KEY: process.env.NEXT_PUBLIC_MAPBOX_API_KEY!,
|
||||
FACEBOOK_APP_ID: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID!,
|
||||
}
|
||||
14
scaffolds/nextjs/core/i18n.ts
Normal file
14
scaffolds/nextjs/core/i18n.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
|
||||
export async function getI18nProps(locale: string | undefined | null, namespaces: string[]) {
|
||||
return serverSideTranslations(
|
||||
locale ?? 'he',
|
||||
Array.from(new Set(['{{ hyphenCase name }}', ...namespaces])),
|
||||
)
|
||||
}
|
||||
|
||||
// export async function getI18nPropsFn(namespaces: string[]) {
|
||||
// return async ({ locale }: NextPageContext) => {
|
||||
// return { props: await serverSideTranslations(locale ?? 'he', namespaces) }
|
||||
// }
|
||||
// }
|
||||
5
scaffolds/nextjs/core/models/user.ts
Normal file
5
scaffolds/nextjs/core/models/user.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface User {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
}
|
||||
48
scaffolds/nextjs/core/routes.ts
Normal file
48
scaffolds/nextjs/core/routes.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
|
||||
const _signup = '/signup'
|
||||
const _flats = '/flats'
|
||||
const _user = '/user'
|
||||
|
||||
export const Routes = {
|
||||
Home: '/',
|
||||
|
||||
// Signup
|
||||
Signup: _signup,
|
||||
SignupUserDetails: `${_signup}/user-details`,
|
||||
|
||||
// User
|
||||
Profile: `${_user}/profile`,
|
||||
UserDetails: `${_user}/details`,
|
||||
Preferences: `${_user}/preferences`,
|
||||
Notifications: `${_user}/notifications`,
|
||||
|
||||
// Flats
|
||||
MyFlat: `${_flats}/my`,
|
||||
MatchedFlatsList: `${_flats}/matches`,
|
||||
PotentialFlatsList: `${_flats}/potential`,
|
||||
LikedFlatsList: `${_flats}/liked`,
|
||||
DislikedFlatsList: `${_flats}/disliked`,
|
||||
ViewFlat: `${_flats}/:id`,
|
||||
}
|
||||
|
||||
type RoutePush = (route: string, params?: { [key: string]: string }) => Promise<boolean>
|
||||
|
||||
export function usePushRoute(): RoutePush {
|
||||
const router = useRouter()
|
||||
const goTo = React.useCallback(
|
||||
(route: string, params?: { [key: string]: string }) =>
|
||||
router.push(buildRoute(route, params ?? {})),
|
||||
[router],
|
||||
)
|
||||
return goTo
|
||||
}
|
||||
|
||||
export function buildRoute(route: string, params: { [key: string]: string }): string {
|
||||
if (!route.includes(':')) {
|
||||
console.warn('Route does not contain any params', route)
|
||||
return route
|
||||
}
|
||||
return Object.entries(params).reduce((acc, [key, value]) => acc.replace(`:${key}`, value), route)
|
||||
}
|
||||
167
scaffolds/nextjs/core/stores/user_store.tsx
Normal file
167
scaffolds/nextjs/core/stores/user_store.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { makeAutoObservable } from 'mobx'
|
||||
import { User } from '../models/user'
|
||||
import userApi, { UserLoginParams, UserRegisterResponse } from '../api/user.api'
|
||||
import { ENV } from '../env'
|
||||
import { Routes, usePushRoute } from '../routes'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export type ProviderType = 'facebook' | 'google'
|
||||
|
||||
export interface ParsedProviderDetails {
|
||||
provider: ProviderType
|
||||
accessToken: string
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
class UserStore {
|
||||
maybeUser: User | null = null
|
||||
provider: ProviderType | null = null
|
||||
/** Token used to login to {{ hyphenCase name }} only - sourced from social login */
|
||||
accessToken: string | null = null
|
||||
/** Token used to auth inside {{ hyphenCase name }} - sourced from `/login` endpoint */
|
||||
idToken: string | null = null
|
||||
loading = true
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
if (ENV.BROWSER_LOADED && localStorage.getItem('idToken')) {
|
||||
this.getStoredIdToken()
|
||||
}
|
||||
if (ENV.BROWSER_LOADED && localStorage.getItem('accessToken')) {
|
||||
this.getStoredAccessToken()
|
||||
}
|
||||
}
|
||||
|
||||
public async login(params: UserLoginParams): Promise<UserRegisterResponse> {
|
||||
this.loading = true
|
||||
const resp = await userApi.login(params)
|
||||
const { user, id_token: idToken } = resp
|
||||
this.setUser(user)
|
||||
this.setIdToken(idToken)
|
||||
this.setAccessToken(params.provider, params.accessToken)
|
||||
this.loading = false
|
||||
return resp
|
||||
}
|
||||
|
||||
public async silentLogin(): Promise<UserRegisterResponse | undefined> {
|
||||
this.loading = true
|
||||
this.getStoredAccessToken()
|
||||
if (this.provider && this.accessToken) {
|
||||
const resp = await this.login({ provider: this.provider, accessToken: this.accessToken })
|
||||
this.loading = false
|
||||
return resp
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
/** Token used to login to {{ hyphenCase name }} only - sourced from social login */
|
||||
public getStoredAccessToken() {
|
||||
const provider = localStorage.getItem('tokenProvider') as ProviderType | null
|
||||
const token = localStorage.getItem('accessToken')
|
||||
if (provider && token) {
|
||||
this.setAccessToken(provider, token)
|
||||
}
|
||||
}
|
||||
|
||||
/** Token used to auth inside {{ hyphenCase name }} - sourced from `/login` endpoint */
|
||||
public getStoredIdToken() {
|
||||
const token = localStorage.getItem('idToken')
|
||||
if (token) {
|
||||
this.setIdToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
public get user(): User {
|
||||
return this.maybeUser!
|
||||
}
|
||||
|
||||
private setUser(user: User) {
|
||||
this.maybeUser = user
|
||||
}
|
||||
|
||||
public get isLoggedIn(): boolean {
|
||||
return Boolean(this.maybeUser)
|
||||
}
|
||||
|
||||
/** Token used to login to {{ hyphenCase name }} only - sourced from social login */
|
||||
private setAccessToken(provider: ProviderType, accessToken: string) {
|
||||
if (ENV.BROWSER_LOADED) {
|
||||
localStorage.setItem('accessToken', accessToken)
|
||||
localStorage.setItem('tokenProvider', provider)
|
||||
}
|
||||
this.accessToken = accessToken
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
/** Token used to auth inside {{ hyphenCase name }} - sourced from `/login` endpoint */
|
||||
private setIdToken(idToken: string) {
|
||||
if (ENV.BROWSER_LOADED) {
|
||||
localStorage.setItem('idToken', idToken)
|
||||
}
|
||||
this.idToken = idToken
|
||||
userApi.setAuthHeaders(idToken)
|
||||
}
|
||||
}
|
||||
|
||||
const _userStore = new UserStore()
|
||||
|
||||
/** Don't forget to wrap in observer if needed */
|
||||
export function useUserStore(): UserStore {
|
||||
return _userStore
|
||||
}
|
||||
|
||||
/** Don't forget to wrap in observer if needed */
|
||||
export function useUser(): User {
|
||||
return useUserStore().user
|
||||
}
|
||||
|
||||
function _useLoginRedirect(force?: boolean): (registered: boolean) => void {
|
||||
const goTo = usePushRoute()
|
||||
const router = useRouter()
|
||||
|
||||
return (registered) => {
|
||||
if (!force && router.pathname !== '/') {
|
||||
return
|
||||
}
|
||||
if (registered || force) {
|
||||
// TODO use [Routes.Home] once redirect is removed
|
||||
goTo(Routes.PotentialFlatsList)
|
||||
} else {
|
||||
goTo(Routes.SignupUserDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useLoginFlow(forceRedirect?: boolean): (details: UserLoginParams) => Promise<void> {
|
||||
const store = useUserStore()
|
||||
const redirect = _useLoginRedirect(forceRedirect)
|
||||
|
||||
return async (details) => {
|
||||
const { registered } = await store.login(details)
|
||||
redirect(registered)
|
||||
}
|
||||
}
|
||||
|
||||
export function useSilentLoginFlow(forceRedirect?: boolean): () => Promise<void> {
|
||||
const store = useUserStore()
|
||||
const redirect = _useLoginRedirect(forceRedirect)
|
||||
const router = useRouter()
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
const resp = await store.silentLogin()
|
||||
if (!resp) {
|
||||
// TODO check public route permission instead of directly checking against [Routes.Home]
|
||||
if (!store.isLoggedIn && router.pathname !== Routes.Home) {
|
||||
router.push(Routes.Signup)
|
||||
}
|
||||
return
|
||||
}
|
||||
redirect(resp.registered)
|
||||
} catch (e) {
|
||||
router.push(Routes.Signup)
|
||||
}
|
||||
}
|
||||
}
|
||||
111
scaffolds/nextjs/core/theme.ts
Normal file
111
scaffolds/nextjs/core/theme.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { alpha } from '@mui/material/styles'
|
||||
import createTheme, { Theme } from '@mui/material/styles/createTheme'
|
||||
|
||||
const colors = {
|
||||
primary: '#638DB3',
|
||||
link: '#0066FF',
|
||||
primaryGradientTop: '#7EACD6',
|
||||
primaryGradientBottom: '#6089AF',
|
||||
disabledBackground: '#bdbdbd',
|
||||
disabled: '#DCDCDC',
|
||||
}
|
||||
|
||||
const shadows = {
|
||||
input: `0 2px 10px ${alpha('#000000', 0.25)}`,
|
||||
card: `0 1px 9px ${alpha('#000000', 0.25)}`,
|
||||
likedCard: `0 1px 9px 1px ${alpha('#0FA639', 0.5)}`,
|
||||
dislikedCard: `0 1px 9px 1px ${alpha('#E22F29', 0.5)}`,
|
||||
mutualCard: `0 1px 9px 1px ${alpha('#0C77DF', 0.5)}`,
|
||||
matchButton: `0 2px 9px 0 ${alpha('#000000', 0.25)}`,
|
||||
avatar: `0 4px 11px ${alpha('#000000', 0.38)}`,
|
||||
}
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
boxShadows: shadows,
|
||||
typography: {
|
||||
fontFamily: 'Assistant, Roboto, Helvetica, sans-serif',
|
||||
},
|
||||
palette: {
|
||||
mode: 'light',
|
||||
primary: {
|
||||
main: colors.primary,
|
||||
},
|
||||
link: {
|
||||
main: colors.link,
|
||||
},
|
||||
disabled: {
|
||||
main: colors.disabled,
|
||||
},
|
||||
disabledBackground: {
|
||||
main: colors.disabledBackground,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiLink: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: colors.link,
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: '24px',
|
||||
fontWeight: 600,
|
||||
borderRadius: '2em',
|
||||
minWidth: '200',
|
||||
'&.Mui-disabled': {
|
||||
background: colors.disabledBackground,
|
||||
},
|
||||
},
|
||||
containedPrimary: {
|
||||
background: `linear-gradient(180deg, ${colors.primaryGradientTop}, ${colors.primaryGradientBottom})`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: '20',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiInputBase: {
|
||||
styleOverrides: {
|
||||
multiline: {
|
||||
borderRadius: '0 !important',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSelect: {
|
||||
defaultProps: {
|
||||
variant: 'outlined',
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: '2em',
|
||||
boxShadow: shadows.input,
|
||||
outline: 'none',
|
||||
},
|
||||
notchedOutline: {
|
||||
border: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
styleOverrides: {
|
||||
h5: {
|
||||
fontSize: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function themeDir(theme: Theme, direction: 'ltr' | 'rtl'): Theme {
|
||||
return { ...theme, direction }
|
||||
}
|
||||
20
scaffolds/nextjs/core/types.ts
Normal file
20
scaffolds/nextjs/core/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface HTMLComponent<T extends HTMLElement>
|
||||
extends React.PropsWithChildren<React.DetailedHTMLProps<React.HTMLAttributes<T>, T>>,
|
||||
CustomComponent {
|
||||
//
|
||||
}
|
||||
|
||||
export type CustomComponent<T = unknown> = T & {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface Option<T = unknown> {
|
||||
value: T
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface ReactOptions<T = unknown> extends Omit<Option<T>, 'label'> {
|
||||
label: React.ReactNode
|
||||
}
|
||||
4
scaffolds/nextjs/core/utils/array_utils.ts
Normal file
4
scaffolds/nextjs/core/utils/array_utils.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function asArray<T extends any[]>(value: T | T[0]): T {
|
||||
if (value === null || value === undefined) return [] as any
|
||||
return Array.isArray(value) ? value : ([value] as any)
|
||||
}
|
||||
14
scaffolds/nextjs/core/utils/error_helpers.ts
Normal file
14
scaffolds/nextjs/core/utils/error_helpers.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import React from 'react'
|
||||
import { FieldErrors } from 'react-hook-form'
|
||||
|
||||
export function useFormErrorText<T>(
|
||||
errors: FieldErrors<T>,
|
||||
): (key: keyof T, error: string) => string | undefined {
|
||||
const fn = React.useCallback(
|
||||
(key: keyof T, error: string) => (errors?.[key] ? error : undefined),
|
||||
[errors],
|
||||
)
|
||||
|
||||
return fn
|
||||
}
|
||||
57
scaffolds/nextjs/core/utils/html_utils.ts
Normal file
57
scaffolds/nextjs/core/utils/html_utils.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { DebouncedFunc } from 'lodash'
|
||||
import { ENV } from '../env'
|
||||
|
||||
export interface UseWindowSizeOptions {
|
||||
type: 'inner' | 'outer'
|
||||
delay?: number
|
||||
}
|
||||
export function useWindowSize({
|
||||
type = 'inner',
|
||||
delay = 100,
|
||||
}: Partial<UseWindowSizeOptions> = {}): {
|
||||
width: number
|
||||
height: number
|
||||
} {
|
||||
const wKey: keyof Window = React.useMemo(() => `${type}Width` as keyof Window, [type])
|
||||
const hKey: keyof Window = React.useMemo(() => `${type}Height` as keyof Window, [type])
|
||||
|
||||
const [windowSize, setWindowSize] = React.useState({
|
||||
width: ENV.BROWSER_LOADED ? window[wKey] : 0,
|
||||
height: ENV.BROWSER_LOADED ? window[hKey] : 0,
|
||||
})
|
||||
|
||||
const handleResize = useDebounce(
|
||||
(): void => setWindowSize({ width: window?.[wKey] ?? 0, height: window?.[hKey] ?? 0 }),
|
||||
delay,
|
||||
[hKey, wKey, delay],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [hKey, handleResize, wKey])
|
||||
|
||||
return windowSize
|
||||
}
|
||||
|
||||
export function useDebounce<T extends (...any: unknown[]) => unknown>(
|
||||
fn: T,
|
||||
delay: number | undefined = undefined,
|
||||
deps: unknown[] = [],
|
||||
): DebouncedFunc<T> {
|
||||
const cb = React.useMemo(() => debounce(fn, delay), [fn, delay])
|
||||
return React.useCallback(cb, [cb, ...deps])
|
||||
}
|
||||
|
||||
export function preventDefault<
|
||||
T extends React.SyntheticEvent,
|
||||
Cb extends (e: T, ...args: unknown[]) => void = (e: T, ...args: unknown[]) => void,
|
||||
>(cb: Cb): Cb {
|
||||
return ((e: T, ...args: unknown[]): void => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
cb(e, ...args)
|
||||
}) as unknown as Cb
|
||||
}
|
||||
16
scaffolds/nextjs/core/utils/image_utils.ts
Normal file
16
scaffolds/nextjs/core/utils/image_utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function fallbackImageUrl(
|
||||
text: string,
|
||||
{ width, height }: Record<'width' | 'height', number>,
|
||||
): string {
|
||||
const qs = new URLSearchParams({
|
||||
bg_color: '008000',
|
||||
fg_color: 'FFFFFF',
|
||||
text,
|
||||
})
|
||||
|
||||
return `https://placeholder.photo/img/${width}x${height}?` + qs.toString()
|
||||
}
|
||||
|
||||
export function apiFallbackImageUrl(url: string): string {
|
||||
return `/api/image?url=${decodeURIComponent(url)}`
|
||||
}
|
||||
6
scaffolds/nextjs/core/utils/locale_utils.ts
Normal file
6
scaffolds/nextjs/core/utils/locale_utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export function useLocaleDirection(): 'ltr' | 'rtl' {
|
||||
const { i18n } = useTranslation()
|
||||
return i18n.dir(i18n.language)
|
||||
}
|
||||
33
scaffolds/nextjs/core/utils/object_utils.ts
Normal file
33
scaffolds/nextjs/core/utils/object_utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { SxProps } from '@mui/material/styles'
|
||||
import { Theme } from '@mui/material/styles/createTheme'
|
||||
import { SystemCssProperties } from '@mui/system/styleFunctionSx'
|
||||
import { Option } from '../types'
|
||||
import { asArray } from './array_utils'
|
||||
|
||||
export function uniqueKeyFromObj<T>(object: T, keys?: Array<keyof T>): string {
|
||||
if (!keys) {
|
||||
keys = Object.keys(object) as Array<keyof T>
|
||||
}
|
||||
return keys.map((key) => String(object[key]).replaceAll(/[^a-z0-9]+/gi, '_')).join('-')
|
||||
}
|
||||
|
||||
export function uniqueKeyFrom(...array: Array<any>): string {
|
||||
return array.map((item) => String(item).replaceAll(/[^a-z0-9]+/gi, '_')).join('-')
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines `sx` props easily
|
||||
* @param sx sx prop for this component
|
||||
* @param rest passed sx props from parent
|
||||
* @returns sx compatible props combined
|
||||
*/
|
||||
export function sxc(
|
||||
sx: SxProps<Theme>,
|
||||
...rest: Array<SxProps<Theme> | SystemCssProperties<Theme> | undefined | false>
|
||||
): SxProps<Theme> {
|
||||
return [...asArray(sx), ...asArray(rest)]
|
||||
}
|
||||
|
||||
export function optionFrom(value: string): Option<string> {
|
||||
return { label: value, value }
|
||||
}
|
||||
3
scaffolds/nextjs/core/utils/promise_utils.ts
Normal file
3
scaffolds/nextjs/core/utils/promise_utils.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function delayedPromise(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
92
scaffolds/nextjs/core/utils/react_utils.ts
Normal file
92
scaffolds/nextjs/core/utils/react_utils.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react'
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'
|
||||
import { ListResult } from '../api/api_types'
|
||||
|
||||
export function useRerender(): () => void {
|
||||
const [, rerender] = React.useState(0)
|
||||
return React.useCallback(() => rerender((r) => r + 1), [rerender])
|
||||
}
|
||||
|
||||
type MaybeKey<K extends string> = `maybe${Capitalize<K>}`
|
||||
|
||||
export type EnhancedQueryResponse<T, K extends keyof T> = UseQueryResult<T> & {
|
||||
[key in MaybeKey<Exclude<K, number | symbol>>]: T[K] | undefined
|
||||
} & {
|
||||
[key in K]: T[K]
|
||||
}
|
||||
|
||||
type EnhancedQueryHook<T, K extends keyof T> = (
|
||||
options?: RequiredQueryOptions<T>,
|
||||
) => EnhancedQueryResponse<T, K>
|
||||
|
||||
export type RequiredQueryOptions<T> = Omit<UseQueryOptions<T>, 'queryKey' | 'queryFn'>
|
||||
|
||||
type StateUpdater<T> = T extends unknown[]
|
||||
? {
|
||||
add: (item: T[number]) => void
|
||||
update: (item: T[number]) => void
|
||||
remove: (item: T[number]) => void
|
||||
}
|
||||
: {
|
||||
set: (item: T) => void
|
||||
}
|
||||
|
||||
type EnhancedQueryOptions<
|
||||
K extends Exclude<keyof T, number | symbol>,
|
||||
T,
|
||||
IKey extends keyof (T[K] extends unknown[] ? T[K][number] : never),
|
||||
> = {
|
||||
cacheKey: string
|
||||
responseKey: K
|
||||
innerKey?: IKey
|
||||
defaultOptions?: RequiredQueryOptions<T>
|
||||
// objectId?: (a: never) => unknown
|
||||
// objectId?: (a: SingleGetter<T, K, IKey>) => unknown
|
||||
}
|
||||
|
||||
type InnerObjTest<T, K extends keyof T> = T[K] extends unknown[] ? T[K][number] : never
|
||||
|
||||
type SingleGetter<
|
||||
T,
|
||||
K extends keyof T,
|
||||
IKey extends keyof InnerObjTest<T, K>,
|
||||
> = T[K] extends unknown[]
|
||||
? IKey extends keyof T[K][number]
|
||||
? T[K][number][IKey]
|
||||
: T[K][number]
|
||||
: IKey extends keyof T[K]
|
||||
? T[K][IKey]
|
||||
: T[K]
|
||||
|
||||
export function createUseApi<
|
||||
T,
|
||||
K extends Exclude<keyof T, number | symbol>,
|
||||
IKey extends keyof (T[K] extends unknown[] ? T[K][number] : never),
|
||||
>(
|
||||
api: () => Promise<T>,
|
||||
{ cacheKey, responseKey, defaultOptions }: EnhancedQueryOptions<K, T, IKey>,
|
||||
): EnhancedQueryHook<T, K> {
|
||||
return (options): EnhancedQueryResponse<T, K> => {
|
||||
const [cache, setCache] = React.useState<T[K]>()
|
||||
|
||||
const { data = {} as T, ...rest } = useQuery([cacheKey], api, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
onSuccess: (data) => {
|
||||
setCache(data[responseKey])
|
||||
options?.onSuccess?.(data)
|
||||
},
|
||||
} as Omit<UseQueryOptions<T>, 'queryKey' | 'queryFn'>)
|
||||
const ent = data[responseKey]!
|
||||
|
||||
const response: EnhancedQueryResponse<T, K> = {
|
||||
...(rest as UseQueryResult<T>),
|
||||
[responseKey]: ent,
|
||||
['maybe' + responseKey[0].toUpperCase() + responseKey.slice(1)]: ent,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: cache as never,
|
||||
}
|
||||
console.log(cacheKey, 'response', response.data, response)
|
||||
return response
|
||||
}
|
||||
}
|
||||
26
scaffolds/nextjs/mui.d.ts
vendored
Normal file
26
scaffolds/nextjs/mui.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { PaletteOptions, PaletteColorOptions } from '@mui/material/styles/createPalette'
|
||||
|
||||
declare module '@mui/material/styles/createTheme' {
|
||||
type Shadows =
|
||||
| 'input'
|
||||
| 'card'
|
||||
| 'likedCard'
|
||||
| 'dislikedCard'
|
||||
| 'mutualCard'
|
||||
| 'matchButton'
|
||||
| 'avatar'
|
||||
interface ThemeOptions {
|
||||
boxShadows: Record<Shadows, string>
|
||||
}
|
||||
|
||||
interface Theme {
|
||||
boxShadows: Record<Shadows, string>
|
||||
}
|
||||
}
|
||||
declare module '@mui/material/styles/createPalette' {
|
||||
export interface PaletteOptions {
|
||||
link?: PaletteColorOptions
|
||||
disabled?: PaletteColorOptions
|
||||
disabledBackground?: PaletteColorOptions
|
||||
}
|
||||
}
|
||||
5
scaffolds/nextjs/next-env.d.ts
vendored
Normal file
5
scaffolds/nextjs/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
9
scaffolds/nextjs/next-i18next.config.js
Normal file
9
scaffolds/nextjs/next-i18next.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
i18n: {
|
||||
defaultLocale: 'he',
|
||||
locales: ['he'],
|
||||
},
|
||||
defaultNS: '{{ hyphenCase name }}',
|
||||
reloadOnPrerender: process.env.NODE_ENV === 'development',
|
||||
}
|
||||
17
scaffolds/nextjs/next.config.js
Normal file
17
scaffolds/nextjs/next.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const withTM = require('next-transpile-modules')(['@mui/material']) // pass the modules you would like to see transpiled
|
||||
const withImages = require('next-images')
|
||||
const { i18n } = require('./next-i18next.config')
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
i18n,
|
||||
images: {
|
||||
domains: ['placeholder.photo'],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = withImages(withTM(nextConfig))
|
||||
61
scaffolds/nextjs/package.json
Normal file
61
scaffolds/nextjs/package.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "{{ hyphenCase name }}",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"gen:page": "npx -y simple-scaffold@latest -t templates/page -s false -o pages ",
|
||||
"gen:component": "npx -y simple-scaffold@latest -t templates/component -s false -o components/atoms",
|
||||
"build:docker": "docker build -t nextjs-docker .",
|
||||
"start:docker": "docker run -p 3100:3000 nextjs-docker"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.9.3",
|
||||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/server": "^11.4.0",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/material": "^5.9.2",
|
||||
"axios": "^0.27.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mapbox-gl": "^2.9.2",
|
||||
"mobx": "^6.6.1",
|
||||
"mobx-react-lite": "^3.4.0",
|
||||
"next": "12.2.3",
|
||||
"next-i18next": "^11.3.0",
|
||||
"next-images": "^1.8.4",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-facebook-login": "^4.1.1",
|
||||
"react-hook-form": "^7.34.0",
|
||||
"react-query": "^3.39.2",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"react-use": "^17.4.0",
|
||||
"stylis": "^4.1.1",
|
||||
"stylis-plugin-rtl": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/mapbox-gl": "^2.7.5",
|
||||
"@types/node": "18.6.2",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/react-facebook-login": "^4.1.5",
|
||||
"@types/stylis": "^4.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.34.0",
|
||||
"@typescript-eslint/parser": "^5.34.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"csv-parse": "^5.3.0",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-config-next": "12.2.3",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
71
scaffolds/nextjs/pages/_app.tsx
Normal file
71
scaffolds/nextjs/pages/_app.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { AppProps } from 'next/app'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import ThemeProvider from '@mui/material/styles/ThemeProvider'
|
||||
import { lightTheme, themeDir } from '../core/theme'
|
||||
import { CacheProvider, EmotionCache } from '@emotion/react'
|
||||
import { createEmotionCache } from '../core/emotion'
|
||||
import { enableStaticRendering } from 'mobx-react-lite'
|
||||
import { appWithTranslation, useTranslation } from 'next-i18next'
|
||||
import nextI18nConfig from '../next-i18next.config'
|
||||
import { QueryClientProvider, QueryClient } from 'react-query'
|
||||
import React from 'react'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import mapboxgl from 'mapbox-gl'
|
||||
import { ENV } from '../core/env'
|
||||
import { useSilentLoginFlow } from '../core/stores/user_store'
|
||||
|
||||
mapboxgl.accessToken = ENV.MAPBOX_API_KEY
|
||||
|
||||
export interface {{ pascalCase name }}AppProps extends AppProps {
|
||||
emotionCache?: EmotionCache
|
||||
}
|
||||
|
||||
let windowInit = false
|
||||
|
||||
const clientSideEmotionCache = createEmotionCache()
|
||||
const isBrowserLoaded = ENV.BROWSER_LOADED
|
||||
enableStaticRendering(!isBrowserLoaded)
|
||||
|
||||
const _{{ pascalCase name }}App: React.FC<{{ pascalCase name }}AppProps> = (props) => {
|
||||
const { Component, pageProps, emotionCache = clientSideEmotionCache } = props
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const themeWithDirection = React.useMemo(
|
||||
() => themeDir(lightTheme, i18n.dir(i18n.language)),
|
||||
[i18n],
|
||||
)
|
||||
const silentLogin = useSilentLoginFlow()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!windowInit && typeof window !== 'undefined') {
|
||||
windowInit = true
|
||||
silentLogin()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typeof window])
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CacheProvider value={emotionCache}>
|
||||
<ThemeProvider theme={themeWithDirection}>
|
||||
<CssBaseline />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const {{ pascalCase name }}App = appWithTranslation(_{{ pascalCase name }}App, nextI18nConfig)
|
||||
|
||||
export default {{ pascalCase name }}App
|
||||
91
scaffolds/nextjs/pages/_document.tsx
Normal file
91
scaffolds/nextjs/pages/_document.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as React from 'react'
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import { createEmotionCache } from '../core/emotion'
|
||||
import createEmotionServer from '@emotion/server/create-instance'
|
||||
|
||||
export default class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="he" dir="rtl">
|
||||
<Head>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="*" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Assistant:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="emotion-insertion-point" content="" />
|
||||
{(this.props as any).emotionStyleTags}
|
||||
</Head>
|
||||
<body>
|
||||
{/* Main page content - deferred */}
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// `getInitialProps` belongs to `_document` (instead of `_app`),
|
||||
// it's compatible with static-site generation (SSG).
|
||||
MyDocument.getInitialProps = async (ctx) => {
|
||||
// Resolution order
|
||||
//
|
||||
// On the server:
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. document.getInitialProps
|
||||
// 4. app.render
|
||||
// 5. page.render
|
||||
// 6. document.render
|
||||
//
|
||||
// On the server with error:
|
||||
// 1. document.getInitialProps
|
||||
// 2. app.render
|
||||
// 3. page.render
|
||||
// 4. document.render
|
||||
//
|
||||
// On the client
|
||||
// 1. app.getInitialProps
|
||||
// 2. page.getInitialProps
|
||||
// 3. app.render
|
||||
// 4. page.render
|
||||
|
||||
const originalRenderPage = ctx.renderPage
|
||||
|
||||
// You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
|
||||
// However, be aware that it can have global side effects.
|
||||
const cache = createEmotionCache()
|
||||
const { extractCriticalToChunks } = createEmotionServer(cache)
|
||||
|
||||
/* eslint-disable */
|
||||
ctx.renderPage = () =>
|
||||
originalRenderPage({
|
||||
enhanceApp: (App) =>
|
||||
function EnhanceApp(props) {
|
||||
const _injectedProps: any = { emotionCache: cache }
|
||||
return <App {..._injectedProps} {...props} />
|
||||
},
|
||||
})
|
||||
/* eslint-enable */
|
||||
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
// This is important. It prevents emotion to render invalid HTML.
|
||||
// See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
|
||||
const emotionStyles = extractCriticalToChunks(initialProps.html)
|
||||
const emotionStyleTags = emotionStyles.styles.map((style) => (
|
||||
<style
|
||||
data-emotion={`${style.key} ${style.ids.join(' ')}`}
|
||||
key={style.key}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: style.css }}
|
||||
/>
|
||||
))
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
// Styles fragment is rendered after the app and page rendering finish.
|
||||
styles: [...React.Children.toArray(initialProps.styles), ...emotionStyleTags],
|
||||
}
|
||||
}
|
||||
13
scaffolds/nextjs/pages/api/hello.ts
Normal file
13
scaffolds/nextjs/pages/api/hello.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
type Data = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data>
|
||||
) {
|
||||
res.status(200).json({ name: 'John Doe' })
|
||||
}
|
||||
25
scaffolds/nextjs/pages/index.tsx
Normal file
25
scaffolds/nextjs/pages/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import { Routes, usePushRoute } from '../core/routes'
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const goTo = usePushRoute()
|
||||
|
||||
React.useEffect(() => {
|
||||
goTo(Routes.Signup)
|
||||
}, [goTo])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
{/* TODO update */}
|
||||
<title>{{ pascalCase name }}</title>
|
||||
<meta name="description" content="{{ pascalCase name }} App" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
BIN
scaffolds/nextjs/public/favicon.ico
Normal file
BIN
scaffolds/nextjs/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
77
scaffolds/nextjs/public/locales/he/flats.json
Normal file
77
scaffolds/nextjs/public/locales/he/flats.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"floor": "קומה",
|
||||
"rooms": "חדרים",
|
||||
"rooms_one": "חדר",
|
||||
"rooms_other": "חדרים",
|
||||
"sqm": "מ\"ר",
|
||||
"city": "עיר",
|
||||
"neighborhood": "שכונה",
|
||||
"street": "רחוב",
|
||||
"apt_number": "מספר דירה",
|
||||
"apt_type": "סוג הדירה",
|
||||
"freetext_placeholder": "הכנס תיאור של הנכס",
|
||||
"apt_type_values": {
|
||||
"apartment": "דירה",
|
||||
"condo": "קונדורה",
|
||||
"house": "בית",
|
||||
"studio": "סטודיו",
|
||||
"townhouse": "דירת עיר",
|
||||
"villa": "בית וילה",
|
||||
"penthouse": "פנטהאוס"
|
||||
},
|
||||
"price": "מחיר",
|
||||
"extras": {
|
||||
"parking": "חניה",
|
||||
"elevator": "מעלית",
|
||||
"balcony": "מרפסת",
|
||||
"renovated": "משופץ",
|
||||
"pets": "חיות",
|
||||
"roommates": "שותפים",
|
||||
"handicappedAccessibility": "גישה לנכים",
|
||||
"furnished": "מרוהטת",
|
||||
"secureRoom": "ממ\"ד"
|
||||
},
|
||||
"my": {
|
||||
"title": "פרטי הדירה שלי",
|
||||
"extras_title": "מה עוד יש לדירה להציע",
|
||||
"photos_title": "העלו עד 6 תמונות",
|
||||
"description_title": "ספר לנו על הדירה שלך",
|
||||
"next": "הבא",
|
||||
"save": "שמור",
|
||||
"required_error": "שדה חובה"
|
||||
},
|
||||
"view": {
|
||||
"freetext_title": "תיאור הנכס"
|
||||
},
|
||||
"view_switcher": {
|
||||
"card": "הצג ככרטיסיות",
|
||||
"list": "הצג כרשימה"
|
||||
},
|
||||
"matched": {
|
||||
"title": "התאמות"
|
||||
},
|
||||
"potential": {
|
||||
"title": "תוצאות"
|
||||
},
|
||||
"liked": {
|
||||
"title": "דירות שאהבתי"
|
||||
},
|
||||
"disliked": {
|
||||
"title": "דירות שלא אהבתי"
|
||||
},
|
||||
"search_preferences": {
|
||||
"title": "מה אני מחפש/ת",
|
||||
"neighborhoods": "שכונות",
|
||||
"selected_neighborhoods": "שכונות שנבחרו",
|
||||
"property_types": "סוגי הדירה",
|
||||
"selected_property_types": "סוגי הדירה שנבחרו",
|
||||
"minPrice": "מחיר מינימלי",
|
||||
"maxPrice": "מחיר מקסימלי",
|
||||
"minRooms": "מס' חדרים מינימלי",
|
||||
"maxRooms": "מס' חדרים מקסימלי",
|
||||
"minSqm": "מגודל (מ\"ר)",
|
||||
"maxSqm": "עד גודל (מ\"ר)",
|
||||
"minFloor": "מקומה",
|
||||
"maxFloor": "עד קומה"
|
||||
}
|
||||
}
|
||||
11
scaffolds/nextjs/public/locales/he/flatswap.json
Normal file
11
scaffolds/nextjs/public/locales/he/flatswap.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app_bar": {
|
||||
"menu": {
|
||||
"results": "תוצאות",
|
||||
"matches": "התאמות",
|
||||
"liked": "דירות שאהבתי",
|
||||
"disliked": "דירות שלא אהבתי",
|
||||
"profile": "פרופיל"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
scaffolds/nextjs/public/locales/he/profile.json
Normal file
8
scaffolds/nextjs/public/locales/he/profile.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"title": "הפרופיל שלי",
|
||||
"buttons": {
|
||||
"details": "פרטים אישיים",
|
||||
"search_preferences": "העדפות חיפוש",
|
||||
"my_apt_details": "פרטי הדירה שלי"
|
||||
}
|
||||
}
|
||||
27
scaffolds/nextjs/public/locales/he/signup.json
Normal file
27
scaffolds/nextjs/public/locales/he/signup.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"select_login": {
|
||||
"title": "קליק אחד ואת/ה בפנים",
|
||||
"subtitle": "אנחנו כבר נדאג לכל השאר...",
|
||||
"accept_terms": "בהצטרפותי אני מסכים ל<a>תנאי השימוש</a>",
|
||||
"buttons": {
|
||||
"facebook": "המשך עם פייסבוק",
|
||||
"google": "המשך עם גוגל"
|
||||
}
|
||||
},
|
||||
"user_details": {
|
||||
"title": "פרטים אישיים",
|
||||
"signup_title": "היי, איזה כיף שאת/ה פה",
|
||||
"subtitle": "כמה פרטים קטנים ונתחיל לחפש לך את הדירה הבאה שלך...",
|
||||
"accept_updates": "אני מעוניין לקבל עדכונים על דירות רלוונטיות והתאמות חדשות באמצעות הדואר האלקטרוני",
|
||||
"submit": "הרשמה",
|
||||
"save": "שמור שינויים",
|
||||
"inputs": {
|
||||
"first_name": "שם פרטי",
|
||||
"last_name": "שם משפחה",
|
||||
"phone": "טלפון",
|
||||
"email": "כתובת מייל",
|
||||
"required_error": "שדה חובה",
|
||||
"phone_error": "לפחות 9 ספרות"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
scaffolds/nextjs/public/logo.svg
Normal file
11
scaffolds/nextjs/public/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.5 KiB |
4
scaffolds/nextjs/public/vercel.svg
Normal file
4
scaffolds/nextjs/public/vercel.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
0
scaffolds/nextjs/scripts/.gitkeep
Normal file
0
scaffolds/nextjs/scripts/.gitkeep
Normal file
12
scaffolds/nextjs/templates/component/{{pascalCase name}}.tsx
Normal file
12
scaffolds/nextjs/templates/component/{{pascalCase name}}.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import Box, { BoxProps } from '@mui/material/Box'
|
||||
import { sxc } from '../../core/utils/object_utils'
|
||||
import { CustomComponent } from '../../core/types'
|
||||
|
||||
export interface {{pascalCase name}}Props extends CustomComponent<BoxProps> {
|
||||
//
|
||||
}
|
||||
|
||||
export const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = ({ sx, children }) => {
|
||||
return <Box sx={sxc({}, sx)}>{children}</Box>
|
||||
}
|
||||
33
scaffolds/nextjs/templates/page/{{snakeCase name}}.tsx
Normal file
33
scaffolds/nextjs/templates/page/{{snakeCase name}}.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import { NextPage, NextPageContext } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getI18nProps } from '../../core/i18n'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Box from '@mui/material/Box'
|
||||
import { PageWrapper } from '../../components/atoms/PageWrapper'
|
||||
|
||||
// NOTE
|
||||
// This page mights need to be SSRed using `getStaticProps`. If that's the case,
|
||||
// we will need to use `getStaticPaths` to generate the routes.
|
||||
|
||||
export const {{pascalCase name}}: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const params = router.query
|
||||
const { t } = useTranslation('{{snakeCase name}}')
|
||||
|
||||
return (
|
||||
<PageWrapper pageTitle="{{ startCase name }}" htmlTitle="{{ pascalCase name }}" htmlDescription="{{ pascalCase name }} App">
|
||||
<Box>
|
||||
{{ pascalCase name }} Page
|
||||
</Box>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ locale }: NextPageContext) {
|
||||
return {
|
||||
props: await getI18nProps(locale, ['{{snakeCase name}}']),
|
||||
}
|
||||
}
|
||||
|
||||
export default {{pascalCase name}}
|
||||
37
scaffolds/nextjs/tsconfig.json
Normal file
37
scaffolds/nextjs/tsconfig.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"mui.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"templates/**/*"
|
||||
]
|
||||
}
|
||||
3344
scaffolds/nextjs/yarn.lock
Normal file
3344
scaffolds/nextjs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
2
scaffolds/tsfiles/.eslintignore
Normal file
2
scaffolds/tsfiles/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
templates/
|
||||
scaffolds/
|
||||
2
scaffolds/tsfiles/.prettierignore
Normal file
2
scaffolds/tsfiles/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
templates/
|
||||
scaffolds/
|
||||
15
scripts/tpl.sh
Normal file
15
scripts/tpl.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
tpl() {
|
||||
case $1 in
|
||||
nextjs)
|
||||
shift
|
||||
npx -y simple-scaffold@latest -t "$DOTFILES/scaffolds/nextjs" -o . $@
|
||||
;;
|
||||
tsfiles)
|
||||
shift
|
||||
npx -y simple-scaffold@latest -t "$DOTFILES/scaffolds/tsfiles" -o . - $@
|
||||
;;
|
||||
*)
|
||||
echo_red "Usage: tpl [nextjs|tsfiles]"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
Reference in New Issue
Block a user