added tons of features

This commit is contained in:
2025-02-05 08:01:14 +02:00
parent 4ae0d8c545
commit 8138da6b1d
195 changed files with 12619 additions and 415 deletions

88
.gitignore vendored
View File

@@ -23,7 +23,7 @@
# misc
.DS_Store
*.pem
*.bak
# debug
npm-debug.log*
yarn-debug.log*
@@ -39,3 +39,89 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/aws.xml
.idea/**/contentModel.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
cmake-build-*/
.idea/**/mongoSettings.xml
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
.idea/replstate.xml
.idea/sonarlint/
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea/httpRequests
.idea/caches/build_file_checksums.ser
logs
*.log
lerna-debug.log*
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pids
*.pid
*.seed
*.pid.lock
lib-cov
coverage
*.lcov
.nyc_output
.grunt
bower_components
.lock-wscript
build/Release
node_modules/
jspm_packages/
web_modules/
.npm
.eslintcache
.stylelintcache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
.node_repl_history
*.tgz
.yarn-integrity
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.cache
.parcel-cache
.next
out
.nuxt
dist
.cache/
.vuepress/dist
.temp
.docusaurus
.serverless/
.fusebox/
.dynamodb/
.tern-port
.vscode-test
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.js
.env*.local
/messages/*.d.json.ts

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/bewell.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

99
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,99 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_GENERATOR_MULT" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="CHAINED_CALL_DOT_ON_NEW_LINE" value="false" />
<option name="SPACES_WITHIN_OBJECT_TYPE_BRACES" value="false" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_GENERATOR_MULT" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_TYPE_BRACES" value="false" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="RIGHT_MARGIN" value="92" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_WRAP" value="5" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
<option name="SOFT_MARGINS" value="92" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="RIGHT_MARGIN" value="127" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" />
<option name="CALL_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
<option name="BINARY_OPERATION_WRAP" value="1" />
<option name="TERNARY_OPERATION_WRAP" value="5" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="ARRAY_INITIALIZER_WRAP" value="5" />
<option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" />
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@10.14.88.14" uuid="44d6c739-7506-4f97-b907-615219cb4f21">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://10.14.88.14:3306</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,8 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLinters/eslint.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/bewell.iml" filepath="$PROJECT_DIR$/.idea/bewell.iml" />
</modules>
</component>
</project>

7
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

8
.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/app/[locale]/(root)/(cabinet)/cabinet/[[...slug]]/page.tsx" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/lib/db/prisma/sql/getUserWithAccount.sql" dialect="MySQL" />
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

50
.prettierrc.json Normal file
View File

@@ -0,0 +1,50 @@
{
"trailingComma": "none",
"tabWidth": 2,
"useTabs": true,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true,
"arrowParens": "avoid",
"bracketSpacing": false,
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrder": [
"<THIRD_PARTY_MODULES>",
"^@api/(.*)$",
"^@app/(.*)$",
"^@components/(.*)$",
"^@config/(.*)$",
"^@constants/(.*)$",
"^@hooks/(.*)$",
"^@services/(.*)$",
"^@shared/(.*)$",
"^@store/(.*)$",
"^@utils/(.*)$",
"^@ui/(.*)$",
"^../(.*)$",
"^./(.*)$",
"^[./]"
],
"overrides": [
{
"files": "*.test.js",
"options": {
"semi": true
}
},
{
"files": [
"*.html",
"legacy/**/*.js"
],
"options": {
"tabWidth": 4
}
}
],
"plugins": [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
]
}

59
actions/admin/category.ts Normal file
View File

@@ -0,0 +1,59 @@
'use server'
import {CategoryLocale} from '@prisma/client'
import {z} from 'zod'
import {getCategoryBySlug} from '@/actions/model/category'
import {i18nLocalesCodes} from '@/i18n-config'
import {STORE_ID} from '@/lib/config/constants'
import {db} from '@/lib/db/prisma/client'
import {createCategoryFormSchema} from '@/lib/schemas/admin/category'
import {cleanEmptyParams, dbErrorHandling, slug as slugger} from '@/lib/utils'
export const onCategoryCreateAction = async (
formData: z.infer<typeof createCategoryFormSchema>
) => {
const validatedData = createCategoryFormSchema.parse(formData)
if (!validatedData) {
return {error: 'Недійсні вхідні дані'}
}
if (validatedData.locales.length < i18nLocalesCodes.length) {
return {error: 'Заповніть всі мови'}
}
const locales: CategoryLocale[] = []
for (const i in validatedData.locales) {
const locale = validatedData.locales[i]
const {title, lang} = locale
const slug = slugger(title, lang)
const result = await getCategoryBySlug({slug, lang})
if (!result) {
const normalized: any = cleanEmptyParams({slug, ...locale})
locales.push(normalized)
} else {
return {error: `Категорія ${title} вже існує`}
}
}
try {
const newCategory = await db.category.create({
data: {
storeId: STORE_ID,
status: true,
// position: 0,
locales: {
create: locales
}
}
})
return {success: JSON.stringify(newCategory, null, 2)}
} catch (error) {
return dbErrorHandling(error)
}
}

129
actions/admin/product.ts Normal file
View File

@@ -0,0 +1,129 @@
'use server'
import {Meta, ProductLocale, ProductToStore} from '@prisma/client'
import sanitizeHtml from 'sanitize-html'
import {z} from 'zod'
import {i18nLocalesCodes} from '@/i18n-config'
import {STORE_ID} from '@/lib/config/constants'
import {getProductBySlug} from '@/lib/data/models/product'
import {db} from '@/lib/db/prisma/client'
import {createProductFormSchema} from '@/lib/schemas/admin/product'
import {
cleanEmptyParams,
dbErrorHandling,
isEmptyObj,
slug as slugger
} from '@/lib/utils'
export const onProductCreateAction = async (
formData: z.infer<typeof createProductFormSchema>
) => {
const validatedData = createProductFormSchema.parse(formData)
if (!validatedData) return {error: 'Недійсні вхідні дані'}
if (validatedData.locales.length < i18nLocalesCodes.length) {
return {error: 'Заповніть всі мови'}
}
const {published, image} = validatedData
const price = parseFloat(validatedData.price).toFixed(2)
const price_promotional = parseFloat(
validatedData.pricePromotional as string
).toFixed(2)
if (parseFloat(price) < 1) {
return {error: 'Ціна не може бути нижчою за одну гривню'}
}
const meta: Meta[] = []
for (const i in validatedData.meta) {
const normalizedMeta: any = cleanEmptyParams(validatedData.meta[i])
//if (!isEmptyObj(normalizedMeta)) {}
meta.push(normalizedMeta)
}
const locales: ProductLocale[] = []
for (const i in validatedData.locales) {
const locale = validatedData.locales[i]
const {title, lang} = locale
const slug = slugger(title, lang)
const result = await getProductBySlug({slug, lang})
if (!result) {
const normalized: any = cleanEmptyParams({slug, ...locale})
//locales.push({...normalized, meta: {create: meta[i]}})
locales.push(normalized)
} else {
return {error: `Продукт з такою назвою ${title} вже існує`}
}
}
try {
const newProduct = await db.product.create({
data: {
image,
toStore: {
create: {
published,
price,
pricePromotional: price_promotional,
storeId: STORE_ID
}
},
locales: {
create: locales
}
}
})
return {success: 'JSON.stringify(newProduct, null, 2)'}
} catch (error) {
return dbErrorHandling(error)
}
}
// const result = sanitizeHtml(description, {
// allowedTags: [
// 'p',
// 'b',
// 'i',
// 'h1',
// 'h2',
// 'h3',
// 'h4',
// 'h5',
// 'h6',
// 'em',
// 'strong',
// 'a',
// 'blockquote',
// 'div',
// 'li',
// 'ol',
// 'ul',
// 'cite',
// 'code',
// 'small',
// 'sub',
// 'sup'
// ],
// nonBooleanAttributes: [],
// allowedAttributes: {
// a: ['href', 'name', 'target'],
// img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading'],
// selfClosing: ['img', 'hr']
// },
// allowedIframeHostnames: [],
// parser: {
// lowerCaseTags: true
// }
// })
//
// console.log(result)

21
actions/auth/common.ts Normal file
View File

@@ -0,0 +1,21 @@
'use server'
import {getUserWithAccount} from '@prisma/client/sql'
import {getAccountByUserId} from '@/data/accout'
import {getUserById} from '@/data/user'
import {db} from '@/lib/db/prisma/client'
export const exisingUser = async (id: string | number) => {
return await getUserById(id)
}
export const exisingUserAccount = async (userId: string | number) => {
return await getAccountByUserId(userId)
}
export const getUserAccountByUserId = async (userId: string | number) => {
const user = await db.$queryRawTyped(getUserWithAccount(userId))
return user.length > 0 ? user[0] : null
}

View File

@@ -0,0 +1,16 @@
'use server'
import {AuthError} from 'next-auth'
import {signIn} from '@/auth'
export async function googleAuthenticate() {
try {
await signIn('google')
} catch (error) {
if (error instanceof AuthError) {
return 'google log in failed'
}
throw error
}
}

87
actions/auth/login.ts Normal file
View File

@@ -0,0 +1,87 @@
'use server'
import {User} from '@prisma/client'
import bcrypt from 'bcryptjs'
import {AuthError} from 'next-auth'
import {getTranslations} from 'next-intl/server'
import * as z from 'zod'
import {signIn} from '@/auth'
import {db} from '@/lib/db/prisma/client'
import {LoginSchema} from '@/lib/schemas'
export const login = async (data: z.infer<typeof LoginSchema>) => {
const t = await getTranslations('Auth')
// Validate the input data
const validatedData = LoginSchema.parse(data)
// If the data is invalid, return an error
if (!validatedData) {
return {error: t('Invalid input data')}
}
// Destructure the validated data
const {email, password} = validatedData
const userExists = await db.user.findFirst({
where: {email}
})
if (!userExists || !userExists.password || !userExists.email) {
return {error: t('Error.user-not-found')}
}
try {
await signIn('credentials', {
email: userExists.email as string,
password: password as string,
redirectTo: '/'
})
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return {error: 'Invalid credentials'}
default:
return {error: error.type}
}
}
throw error
}
return {success: 'User successfully logged in!'}
}
export const authorizeCallback = async (
credentials: Partial<Record<'email' | 'password', unknown>>
): Promise<User | null> => {
const validatedData = z
.object({
email: z.string().email(),
password: z.string().min(6)
})
.safeParse(credentials)
if (!validatedData.success) return null
const {email, password} = validatedData.data
const user = await db.user.findFirst({
where: {email}
})
if (!user || !user.password || !user.email) return null
try {
if (await bcrypt.compare(password, user.password)) {
return user
} else {
console.log('Invalid credentials', user.email)
return null
}
} catch (err) {
console.log('Verifying password error', err)
}
return null
}

79
actions/auth/register.ts Normal file
View File

@@ -0,0 +1,79 @@
'use server'
import bcrypt from 'bcryptjs'
import * as z from 'zod'
import {db} from '@/lib/db/prisma/client'
import {RegisterSchema} from '@/lib/schemas'
import {hashPassword} from '@/lib/utils'
// import { generateVerificationToken } from "@/lib/token";
// import { sendVerificationEmail } from "@/lib/mail";
export const register = async (data: z.infer<typeof RegisterSchema>) => {
try {
// Validate the input data
const validatedData = RegisterSchema.parse(data)
// If the data is invalid, return an error
if (!validatedData) {
return {error: 'Invalid input data'}
}
// Destructure the validated data
const {email, name, password, passwordConfirmation} = validatedData
// Check if passwords match
if (password !== passwordConfirmation) {
return {error: 'Passwords do not match'}
}
// Hash the password
const hashedPassword = await hashPassword(password)
// Check to see if user already exists
const userExists = await db.user.findFirst({
where: {
email
}
})
// If the user exists, return an error
if (userExists) {
return {error: 'Email already is in use. Please try another one.'}
}
const lowerCaseEmail = email.toLowerCase()
// Create the user
const user = await db.user.create({
data: {
email: lowerCaseEmail,
name,
password: hashedPassword
}
})
// Generate Verification Token
// const verificationToken = await generateVerificationToken(email);
// await sendVerificationEmail(lowerCaseEmail, verificationToken.token);
return {success: 'Email Verification was sent'}
} catch (error) {
// Handle the error, specifically check for a 503 error
console.error('Database error:', error)
if ((error as {code: string}).code === 'ETIMEDOUT') {
return {
error: 'Unable to connect to the database. Please try again later.'
}
} else if ((error as {code: string}).code === '503') {
return {
error: 'Service temporarily unavailable. Please try again later.'
}
} else {
return {error: 'An unexpected error occurred. Please try again later.'}
}
}
}

View File

@@ -0,0 +1,31 @@
import {isRedirectError} from 'next/dist/client/components/redirect-error'
type Options<T> = {
actionFn: () => Promise<T>
successMessage?: string
}
const executeAction = async <T>({
actionFn,
successMessage = 'The actions was successful'
}: Options<T>): Promise<{success: boolean; message: string}> => {
try {
await actionFn()
return {
success: true,
message: successMessage
}
} catch (error) {
if (isRedirectError(error)) {
throw error
}
return {
success: false,
message: 'An error has occurred during executing the action'
}
}
}
export {executeAction}

17
actions/model/category.ts Normal file
View File

@@ -0,0 +1,17 @@
'use server'
import {CategoryLocale, Lang} from '@prisma/client'
import {db} from '@/lib/db/prisma/client'
export const getCategoryBySlug = async (data: {
slug: string
lang: string
}): Promise<CategoryLocale | null> => {
return db.categoryLocale.findFirst({
where: {
slug: data.slug,
lang: data.lang as Lang
}
})
}

View File

@@ -0,0 +1,30 @@
'use server'
import {auth} from '@/auth'
import {
Access,
AllRolesPermissions,
PERMISSIONS,
type Permission,
type SingedInSession
} from '@/lib/permission'
export type CanAccessResponse = {can: boolean; session: SingedInSession | null}
export type CanResponse = boolean | CanAccessResponse
const can = async (permission: Permission): Promise<CanResponse> => {
const session: SingedInSession = (await auth()) as SingedInSession
if (!session) return false
const able =
PERMISSIONS[session.user.role as keyof AllRolesPermissions].includes(
permission
)
return !Object.values(Access).includes(permission as Access)
? able
: {can: able, session}
}
export default can

View File

@@ -0,0 +1,19 @@
import {auth} from '@/auth'
import {CreateForm} from '@/components/(protected)/admin/category/create-form'
import {dump} from '@/lib/utils'
export default async function Page({
params
}: {
params: Promise<{slug?: string[]}>
}) {
const session = await auth()
const {slug} = await params
switch ((slug || [])[0]) {
case 'create':
return <CreateForm />
}
return <div>{dump(slug)}</div>
}

View File

@@ -0,0 +1,14 @@
import Link from 'next/link'
import AdminPermission from '@/components/(protected)/admin/auth/permission'
export default function AdminCategoryPage() {
return (
<div>
<AdminPermission />
<p>
<Link href='/admin/category/create'>Створити</Link>
</p>
</div>
)
}

View File

@@ -0,0 +1,65 @@
import {cookies} from 'next/headers'
import {ReactNode} from 'react'
import {auth} from '@/auth'
import AdminPermission from '@/components/(protected)/admin/auth/permission'
import {AdminSidebar} from '@/components/(protected)/admin/sidebar'
import {
SidebarInset,
SidebarProvider,
SidebarTrigger
} from '@/components/ui/sidebar'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@/ui/breadcrumb'
import {Separator} from '@/ui/separator'
export default async function AdminLayout({children}: {children: ReactNode}) {
//const session = await auth()
if (!(await auth())) return <AdminPermission />
const cookieStore = await cookies()
const defaultOpen = cookieStore.get('sidebar:state')?.value === 'true'
return (
<SidebarProvider
defaultOpen={defaultOpen}
style={{
// @ts-ignore
'--sidebar-width': '16rem',
'--sidebar-width-mobile': '18rem'
}}
>
<AdminSidebar />
<SidebarInset>
<header className='flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12'>
<div className='flex items-center gap-2 px-4'>
<SidebarTrigger className='-ml-1' />
<Separator orientation='vertical' className='mr-2 h-4' />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className='hidden md:block'>
<BreadcrumbLink href='#'>
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className='hidden md:block' />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<main id='admin-bw-panel' className='container'>
{children}
</main>
</SidebarInset>
</SidebarProvider>
)
}

View File

@@ -0,0 +1,5 @@
import AdminPermission from '@/components/(protected)/admin/auth/permission'
export default async function AdminPage() {
return <AdminPermission />
}

View File

@@ -0,0 +1,30 @@
import ProductCreateEditForm from '@/components/(protected)/admin/product/create-edit-form'
import {getProductById} from '@/lib/data/models/product'
import {dump} from '@/lib/utils'
export default async function Page({
params
}: {
params: Promise<{slug?: string[]}>
}) {
const {slug} = await params
const [method, id] = slug || []
let data = null
if (id) {
data = await getProductById(parseInt(id))
if (data) {
data = JSON.parse(JSON.stringify(data))
}
}
switch (method) {
case 'create':
return <ProductCreateEditForm />
case 'update':
return <ProductCreateEditForm data={data} />
default:
return <div>{dump(slug)}</div>
}
}

View File

@@ -0,0 +1,34 @@
import {Product} from '@prisma/client'
import {LayoutList} from 'lucide-react'
import Link from 'next/link'
import AdminPermission from '@/components/(protected)/admin/auth/permission'
import dayjs from '@/lib/config/dayjs'
import {getProducts} from '@/lib/data/models/product'
import {dump} from '@/lib/utils'
//const products = await getProducts()
export default async function AdminProductPage() {
return (
<>
<AdminPermission />
<p>
<Link href='/admin/product/create'>Створити</Link>
</p>
{/*<section className={'mt-12'}>
{products
? products.map((product: Product) => (
<article
key={product.id}
className={'flex flex-row items-center justify-evenly'}
>
<LayoutList />
{product.locales[0].headingTitle || product.locales[0].title}
</article>
))
: null}
</section>*/}
</>
)
}

View File

@@ -0,0 +1,16 @@
import React from 'react'
export default async function AuthLayout({
children
}: {
children: React.ReactNode
}) {
return (
<section className='relative w-full'>
<div className='flex h-screen items-center justify-center bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-brand-violet-400 to-brand-yellow-200'>
{/**/}
{children}
</div>
</section>
)
}

View File

@@ -0,0 +1,5 @@
import LoginForm from '@/components/auth/forms/login-form'
export default function LoginPage() {
return <LoginForm />
}

View File

@@ -0,0 +1,5 @@
import RegisterForm from '@/components/auth/forms/register-form'
export default function RegisterPage() {
return <RegisterForm />
}

View File

@@ -0,0 +1,26 @@
import can, {CanAccessResponse} from '@/actions/permission'
import LoginForm from '@/components/auth/forms/login-form'
import CabinetIndex from '@/components/cabinet'
import {Access} from '@/lib/permission'
export default async function CabinetPage({
params
}: {
params: Promise<{slug?: string[]}>
}) {
const user = (await can(Access.Cabinet)) as CanAccessResponse
if (!user.can || !user.session) {
return (
<div className='my-8'>
<div className='container flex flex-col sm:flex-row'>
<LoginForm />
</div>
</div>
)
} else {
const {slug} = await params
return <CabinetIndex slug={slug} session={user.session} />
}
}

View File

@@ -0,0 +1,12 @@
import {Metadata} from 'next'
export const metadata: Metadata = {
title: 'Checkout'
}
export default function CheckoutPage() {
//throw new Error('NOT IMPLEMENTED')
//const session = await auth()
return <div>CheckoutPage</div>
}

View File

@@ -0,0 +1,17 @@
import {ReactNode} from 'react'
import Above from '@/components/shared/above'
import Footer from '@/components/shared/footer'
import Header from '@/components/shared/header'
export default async function HomeLayout({children}: {children: ReactNode}) {
return (
<>
<Above />
<Header />
{/*<Above />*/}
{children}
<Footer />
</>
)
}

View File

@@ -0,0 +1,69 @@
import Image from 'next/image'
import FeatureCards from '@/components/shared/home/feature-cards'
import {HomeCarousel} from '@/components/shared/home/home-carousel'
import AppCatalog from '@/components/shared/sidebar/app-catalog'
import {carousels} from '@/lib/data'
import {db} from '@/lib/db/prisma/client'
import {dump} from '@/lib/utils'
import image from '@/public/uploads/products/IMG_6572.jpg'
// const storeModel = async (id: any) => {
// return db.store.findFirst({
// where: {id},
// include: {
// storeLocale: {
// include: {
// meta: {
// include: {
// openGraph: true
// }
// }
// }
// }
// }
// })
// }
export default async function HomePage() {
return (
<>
<div className='mt-1'>
<div className='container flex flex-col sm:flex-row'>
<section className='bw-layout-col-left pt-3'>
<AppCatalog />
</section>
<div className='bw-layout-col-right pt-3'>
{/*<pre>{dump(await storeModel(1))}</pre>*/}
<section className='w-full'>
<HomeCarousel items={carousels}></HomeCarousel>
</section>
</div>
</div>
</div>
{/*<pre>{JSON.stringify(session)}</pre>*/}
<section className='relative mx-auto mt-8 h-[640px] w-[840px] bg-brand-violet-200'>
<Image
src={'/uploads/products/IMG_6572.jpg'}
//fill
//sizes='(min-width: 808px) 50vw, 100vw'
width={1280}
height={1280}
alt=''
title=''
priority
style={{
objectFit: 'contain' // cover, contain, none
}}
/>
</section>
<section className='mb-4 mt-[128px]'>
<div className='container'>
<FeatureCards />
</div>
</section>
</>
)
}

34
app/[locale]/error.tsx Normal file
View File

@@ -0,0 +1,34 @@
'use client'
import {useTranslations} from 'next-intl'
import React from 'react'
import {Button} from '@/components/ui/button'
export default function ErrorPage({
error,
reset
}: {
error: Error
reset: () => void
}) {
const t = useTranslations('Error')
return (
<div className='flex min-h-screen flex-col items-center justify-center'>
<div className='w-1/3 rounded-lg p-6 text-center shadow-md'>
<h1 className='mb-4 text-3xl font-bold'>{t('title')}</h1>
<p className='text-destructive'>{error.message}</p>
<Button variant='outline' className='mt-4' onClick={() => reset()}>
{t('try-again')}
</Button>
<Button
variant='outline'
className='ml-2 mt-4'
onClick={() => (window.location.href = '/')}
>
{t('back-to-home')}
</Button>
</div>
</div>
)
}

33
app/[locale]/layout.tsx Normal file
View File

@@ -0,0 +1,33 @@
import {NextIntlClientProvider} from 'next-intl'
import {getMessages} from 'next-intl/server'
import {notFound} from 'next/navigation'
import {ReactNode} from 'react'
import {routing} from '@/i18n/routing'
import {TIMEZONE} from '@/lib/constants'
export default async function RootLayout({
children,
params
}: Readonly<{
children: ReactNode
params: Promise<{locale: string}>
}>) {
const {locale} = await params
if (!routing.locales.includes(locale as any)) {
notFound()
}
const messages = await getMessages()
//const queryClient = new QueryClient()
return (
<NextIntlClientProvider
messages={messages}
timeZone={TIMEZONE}
now={new Date()}
>
{children}
</NextIntlClientProvider>
)
}

View File

@@ -0,0 +1 @@
export {GET, POST} from '@/auth'

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,20 +2,215 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
margin: 0;
padding: 0;
overflow-y: scroll;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 4.615% 12.75%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 47.9 95.8% 53.1%;
--primary-foreground: 26 83.3% 14.1%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 47.9 95.8% 53.1%;
--primary-foreground: 26 83.3% 14.1%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 35.5 91.7% 32.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@layer utilities {
/* .no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; !* IE and Edge *!
scrollbar-width: none; !* Firefox *!
}*/
.bw-app-catalog-collapse {
@apply absolute scale-0 z-20
}
.header-button {
@apply cursor-pointer p-1 border border-transparent rounded-[2px];
}
.h1-bold {
@apply font-bold text-2xl lg:text-3xl;
}
.bw-layout-col-left {
@apply flex-1 sm:w-7/12 md:w-5/12 xl:w-4/12 lg:flex-col
}
.bw-layout-col-right {
@apply sm:w-5/12 md:w-7/12 xl:w-8/12 flex-1 sm:flex-auto sm:pl-4 md:pl-7 xl:pl-9
}
.bw-header-col-left {
@apply w-[9/12] flex-auto
}
.bw-header-col-right {
@apply flex-grow-0 flex-shrink-0 md:basis-[272px]
}
.bw-border-color {
@apply border-brand-violet-100;
}
.bw-separator-color {
@apply bg-brand-violet-100;
}
}
.bw-dd-menu {
/* since nested groupes are not supported we have to use
regular css for the nested dropdowns
*/
li>ul { transform: translatex(100%) scale(0) }
li:hover>ul { transform: translatex(101%) scale(1) }
li > button svg { transform: rotate(-90deg) }
li:hover > button svg { transform: rotate(-270deg) }
/* Below styles fake what can be achieved with the tailwind config
you need to add the group-hover variant to scale and define your custom
min width style.
See https://codesandbox.io/s/tailwindcss-multilevel-dropdown-y91j7?file=/index.html
for implementation with config file
*/
.group:hover .group-hover\:scale-100 { transform: scale(1) }
.group:hover .group-hover\:-rotate-180 { transform: rotate(180deg) }
}
.jodit-wysiwyg > * {
all: revert;
color: #262626 !important;
}
.jodit-wysiwyg {
padding: 0 16px !important;
/*font-family: Consolas, Monaco, sans-serif;*/
/*p {
font-size: 17px !important;
}*/
}
#admin-bw-panel form {
input {
@apply bg-white outline-0 text-[16px] leading-none;
}
[role="tablist"] {
@apply bg-brand-violet ;
[data-state=active] {
@apply bg-brand-yellow text-brand-violet;
}
[data-state=inactive] {
@apply text-brand-yellow bg-brand-violet;
}
/*@apply bg-brand-yellow-100;*/
/*[id$='trigger-uk' i][data-state=active] {
@apply bg-brand-yellow-100;
}
[id$='trigger-ru' i][data-state=active] {
@apply bg-brand-violet-100;
}*/
}
label{
@apply uppercase text-brand-violet-950 flex items-center justify-between leading-none ml-1 mt-4;
}
#form-tab-uk label {
&:after {
content: ' ';
display: inline-block;
width: 18px;
height: 0;
border-top: solid #0066cc 6px;
border-bottom: solid #ffcc00 6px;
margin-right: 4px;
}
}
}

View File

@@ -1,34 +1,32 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import type {Metadata} from 'next'
import {headers} from 'next/headers'
import {ReactNode} from 'react'
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
import './globals.css'
import {Toaster} from '@/components/ui/toaster'
import {routing} from '@/i18n/routing'
import {APP_DESCRIPTION, APP_NAME, APP_SLOGAN} from '@/lib/constants'
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
title: {
template: `%s | ${APP_NAME}`,
default: `${APP_NAME}. ${APP_SLOGAN}`
},
description: APP_DESCRIPTION
}
export default async function RootLayout({
children
}: Readonly<{children: ReactNode}>) {
const headersList = await headers()
const locale = headersList.get('x-site-locale') ?? routing.defaultLocale
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<html lang={locale} suppressHydrationWarning>
<body className='min-h-screen antialiased'>
{children}
<Toaster />
</body>
</html>
);
)
}

View File

@@ -1,101 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

20
auth.config.ts Normal file
View File

@@ -0,0 +1,20 @@
import type {NextAuthConfig} from 'next-auth'
//import 'server-only'
export default {
// pages: {
// signIn: '/auth/login',
// //signOut: '/auth/register',
// newUser: '/auth/register',
// error: '/auth/error',
// verifyRequest: '/auth/verify-request'
// },
callbacks: {
authorized({auth}) {
return !!auth?.user
}
},
providers: []
} satisfies NextAuthConfig

101
auth.ts Normal file
View File

@@ -0,0 +1,101 @@
import {PrismaAdapter} from '@auth/prisma-adapter'
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import Github from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
//import 'server-only'
import authConfig from './auth.config'
import {
getUserAccountByUserId,
exisingUser as getUserById
} from '@/actions/auth/common'
import {authorizeCallback} from '@/actions/auth/login'
import {db} from '@/lib/db/prisma/client'
export const {
auth,
handlers: {GET, POST},
signIn,
signOut
} = NextAuth({
pages: {
signIn: '/login',
//signOut: '/auth/signout',
//error: '/auth/error', // Error code passed in query string as ?error=
//verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: '/register'
},
trustHost: true,
logger:
process.env.NODE_ENV === 'development'
? {
// debug: console.log,
error: console.error,
warn: console.warn
}
: {error: console.error},
debug: process.env.NODE_ENV === 'development',
session: {strategy: 'jwt', maxAge: 48 * 60 * 60},
...authConfig,
adapter: PrismaAdapter(db),
providers: [
Google,
Github,
Credentials({
name: 'credentials',
credentials: {
email: {label: 'Email', type: 'text'},
password: {label: 'Пароль', type: 'password'}
},
async authorize(credentials): Promise<any | null> {
return await authorizeCallback(credentials)
}
})
],
callbacks: {
async signIn({user, account}) {
if (account?.provider !== 'credentials') {
return true
}
if (!user?.id) return false
const exisingUser = await getUserById(user.id)
return !Boolean(exisingUser?.emailVerified)
},
async jwt({token}) {
if (!token?.sub) return token
const exisingUser = await getUserAccountByUserId(token.sub)
if (!exisingUser) return token
token.isOauth = Boolean(exisingUser.isOauth)
token.provider = exisingUser.provider
token.role = exisingUser.role
token.profileId = exisingUser.profileId
token.name = exisingUser.name
token.username = exisingUser.username
token.email = exisingUser.email
token.image = exisingUser.image
return token
},
async session({token, session}) {
return {
...session,
user: {
...session.user,
id: token.sub,
isOauth: token.isOauth,
provider: token.provider,
role: token.role,
profileId: token.profileId,
username: token.username
}
}
}
}
})

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,26 @@
import {UserRole} from '@prisma/client'
import {notFound} from 'next/navigation'
import {auth} from '@/auth'
import LoginForm from '@/components/auth/forms/login-form'
import {SessionUser} from '@/types/auth'
export default async function AdminPermission() {
const session = await auth()
const user: SessionUser = session?.user as unknown as SessionUser
if (!user) {
return (
<div className='my-8'>
<div className='container flex flex-col sm:flex-row'>
<LoginForm />
</div>
</div>
)
}
//if (![UserRole.CUSTOMER].includes(user.role as 'CUSTOMER')) {
if (user.role !== UserRole.SUPERVISOR) {
notFound()
}
}

View File

@@ -0,0 +1,171 @@
'use client'
//https://codesandbox.io/p/sandbox/react-hook-form-zod-with-array-of-objects-field-array-usefieldarray-8xh3ry?file=%2Fsrc%2FApp.tsx%3A11%2C53
// https://stackoverflow.com/questions/78004655/how-to-dynamically-add-array-of-objects-to-react-hook-form
import {zodResolver} from '@hookform/resolvers/zod'
import {useState} from 'react'
import {useFieldArray, useForm} from 'react-hook-form'
import {z} from 'zod'
import {onCategoryCreateAction} from '@/actions/admin/category'
import FormError from '@/components/auth/form-error'
import {FormSuccess} from '@/components/auth/form-success'
import {createCategoryFormSchema as validationSchema} from '@/lib/schemas/admin/category'
import {dump} from '@/lib/utils'
import {ResourceMessages} from '@/types'
import {Button} from '@/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/ui/form'
import {Input} from '@/ui/input'
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs'
export const CreateForm = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const localesValues = {
title: '',
short_title: '',
description: ''
}
const form = useForm<z.infer<typeof validationSchema>>({
resolver: zodResolver(validationSchema),
mode: 'onBlur',
defaultValues: {
locales: [
{lang: 'uk', ...localesValues},
{lang: 'ru', ...localesValues}
]
}
})
const {fields, append} = useFieldArray({
name: 'locales',
control: form.control
})
const onSubmit = async (data: z.infer<typeof validationSchema>) => {
setLoading(true)
onCategoryCreateAction(data).then((res: ResourceMessages) => {
if (res?.error) {
setError(res?.error)
setSuccess('')
setLoading(false)
} else {
setSuccess(res?.success as string)
setError('')
setLoading(false)
}
})
}
return (
<Form {...form}>
<form action='' className='my-8' onSubmit={form.handleSubmit(onSubmit)}>
<div className='mx-auto w-[400px]'>
<Tabs defaultValue='uk'>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='uk'>Українська</TabsTrigger>
<TabsTrigger value='ru'>російська</TabsTrigger>
</TabsList>
{fields.map((_, index) => (
<TabsContent
value={form.getValues(`locales.${index}.lang`)}
key={index}
className='space-y-8'
>
<FormField
control={form.control}
key={index}
name={`locales.${index}.lang`}
render={({field}) => (
<FormItem className={'w-full'}>
{/*<FormLabel>Мова</FormLabel>*/}
<FormControl>
<Input type='hidden' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 1}
name={`locales.${index}.title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Назва категорії</FormLabel>
<FormControl>
<Input
lang={form.getValues(`locales.${index}.lang`)}
placeholder=''
{...field}
/>
</FormControl>
{/*<FormDescription>
Select a language between uk or ru
</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 2}
name={`locales.${index}.short_title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Скорочена назва категорії</FormLabel>
<FormControl>
<Input
lang={form.getValues(`locales.${index}.lang`)}
placeholder=''
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 3}
name={`locales.${index}.description`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Опис категорії</FormLabel>
<FormControl>
<Input
lang={form.getValues(`locales.${index}.lang`)}
placeholder=''
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
))}
</Tabs>
<div className='my-8'>
<FormError message={error} />
<FormSuccess message={success} />
<Button type='submit'>
{loading ? 'Додаємо до бази...' : 'Створити'}
</Button>
</div>
</div>
</form>
</Form>
)
}

View File

@@ -0,0 +1,47 @@
import {DropdownMenuTrigger} from '@radix-ui/react-dropdown-menu'
import {ChevronUp, User2} from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem
} from '@/ui/dropdown-menu'
import {
SidebarFooter,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
} from '@/ui/sidebar'
export default function AdminSidebarFooter() {
return (
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
<User2 /> Username
<ChevronUp className='ml-auto' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side='top'
className='w-[--radix-popper-anchor-width]'
>
<DropdownMenuItem>
<span>Account</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Billing</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Sign out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
)
}

View File

@@ -0,0 +1,41 @@
import {ChevronDown} from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/ui/dropdown-menu'
import {
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
} from '@/ui/sidebar'
export default function AdminSidebarHeader() {
return (
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton>
Select Workspace
<ChevronDown className='ml-auto' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-[--radix-popper-anchor-width]'>
<DropdownMenuItem>
<span>Acme Inc</span>
</DropdownMenuItem>
<DropdownMenuItem>
<span>Acme Corp.</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
)
}

View File

@@ -0,0 +1,442 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import dynamic from 'next/dynamic'
import React, {useEffect, useMemo, useRef, useState} from 'react'
import {useFieldArray, useForm} from 'react-hook-form'
import {z} from 'zod'
import {onProductCreateAction} from '@/actions/admin/product'
import {useToast} from '@/hooks/use-toast'
import {i18nDefaultLocale, i18nLocales} from '@/i18n-config'
import {BaseEditorConfig} from '@/lib/config/editor'
import {createProductFormSchema} from '@/lib/schemas/admin/product'
import {toEmptyParams} from '@/lib/utils'
import {Button} from '@/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/ui/form'
import {Input} from '@/ui/input'
import {Switch} from '@/ui/switch'
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs'
const JoditEditor = dynamic(() => import('jodit-react'), {ssr: false})
let localesValues = {
title: '',
shortTitle: '',
headingTitle: '',
description: '',
content: '',
instruction: ''
}
let metaValues = {
title: '',
description: '',
keywords: '',
author: ''
}
export default function ProductCreateEditForm({data}: {data?: any}) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [description0, setDescription0] = useState(
data?.locales[0].description || ''
)
const [description1, setDescription1] = useState(
data?.locales[1].description || ''
)
const [content0, setContent0] = useState(data?.locales[0].content || '')
const [content1, setContent1] = useState(data?.locales[1].content || '')
const [instruction0, setInstruction0] = useState(
data?.locales[0].instruction || ''
)
const [instruction1, setInstruction1] = useState(
data?.locales[1].instruction || ''
)
const editor = useRef(null) //declared a null value
const {toast} = useToast()
const config = useMemo(() => BaseEditorConfig, [])
const form = useForm<z.infer<typeof createProductFormSchema>>({
resolver: zodResolver(createProductFormSchema),
mode: 'onBlur',
defaultValues: data
? (data => {
const {locales, meta} = data
return {
published: data.toStore[0].published,
price: data.toStore[0].price,
pricePromotional: data.toStore[0].pricePromotional,
image: data.image,
locales: toEmptyParams(locales) as any,
meta: meta
? (toEmptyParams(meta) as any)
: [{...metaValues}, {...metaValues}]
}
})(data)
: {
published: false,
price: '0',
pricePromotional: '0',
image: '',
locales: [
{lang: 'uk', ...localesValues},
{lang: 'ru', ...localesValues}
],
meta: [{...metaValues}, {...metaValues}]
}
})
const {register, setValue} = form
useEffect(() => {
register('locales.0.description')
register('locales.0.content')
register('locales.0.instruction')
register('locales.1.description')
register('locales.1.content')
register('locales.1.instruction')
}, [register])
const {fields: localeFields} = useFieldArray({
name: 'locales',
control: form.control
})
const {fields: metaFields} = useFieldArray({
name: 'meta',
control: form.control
})
console.log(form.formState.errors)
const onSubmit = async (values: z.infer<typeof createProductFormSchema>) => {
setLoading(true)
onProductCreateAction(values).then((res: any) => {
if (res?.error) {
setError(res?.error)
setSuccess('')
setLoading(false)
toast({
variant: 'destructive',
title: res?.error,
description: res?.message
})
} else {
setSuccess(res?.success as string)
setError('')
setLoading(false)
toast({
variant: 'success',
title: res?.success,
description: res?.message
})
}
})
}
return (
<Form {...form}>
<form
action=''
onSubmit={form.handleSubmit(onSubmit)}
className='bgs-grasy-50/50 mx-auto mb-8 min-w-[640px] max-w-[992px] flex-1 space-y-5 rounded-lg border p-4'
>
<div className='mx-auto my-4 w-full space-y-4'>
<h1 className='mb-6 text-center text-2xl font-bold text-brand-violet'>
ДОДАВАННЯ ТОВАРУ ДО БАЗИ
</h1>
<div className='my-4 space-y-4'>
<FormField
control={form.control}
name='published'
render={({field}) => (
<FormItem className='flex flex-row items-center justify-between rounded-lg border bg-gray-50 p-4'>
<div className='space-y-0.5'>
<FormLabel className='text-base'>Опублікувати</FormLabel>
<FormDescription>
Відразу після збереження буде розміщено на сайті
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<div className='flex flex-row items-center justify-between gap-4 rounded-lg border bg-gray-50 p-4'>
<div className='w-1/2'>
<FormField
control={form.control}
name='price'
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Ціна за одиницю товару</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='w-1/2'>
<FormField
control={form.control}
name='pricePromotional'
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Акційна Ціна</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<fieldset className='flex gap-4 rounded-lg border bg-gray-50 p-4'>
<FormField
control={form.control}
name='image'
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Головне зображення</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormDescription>
Вкажіть шліх до зображення відносно публічної папки
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</fieldset>
</div>
<Tabs
defaultValue={i18nDefaultLocale}
className='min-h-[560px] rounded-lg border p-4'
>
<TabsList className='grid w-full grid-cols-2'>
{i18nLocales.map(locale => (
<TabsTrigger key={locale.icon} value={locale.code}>
{locale.nameUkr}
</TabsTrigger>
))}
</TabsList>
{localeFields.map((_, index) => (
<TabsContent
id={`form-tab-${form.getValues(`locales.${index}.lang`)}`}
value={form.getValues(`locales.${index}.lang`)}
key={index}
className='space-y-4'
>
<FormField
control={form.control}
key={index}
name={`locales.${index}.lang`}
render={({field}) => (
<FormItem className={'w-full'}>
{/*<FormLabel>Мова</FormLabel>*/}
<FormControl>
<Input type='hidden' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<fieldset className='flex gap-4 rounded-lg border bg-gray-50 p-4'>
<div className='w-1/2'>
<FormField
control={form.control}
key={index + 1}
name={`locales.${index}.title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Назва товару</FormLabel>
<FormControl>
<Input placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='w-1/2'>
<FormField
control={form.control}
key={index + 2}
name={`locales.${index}.shortTitle`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Скорочена назва товару</FormLabel>
<FormControl>
<Input placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</fieldset>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<FormField
control={form.control}
key={index + 3}
name={`locales.${index}.headingTitle`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>
Назва товару у описі та коротка анотація
</FormLabel>
<FormControl>
<Input placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<JoditEditor
key={index + 4}
ref={editor}
config={config}
value={index === 0 ? description0 : description1}
className='mt-4 w-full'
onBlur={value => {
index === 0
? setDescription0(value)
: setDescription1(value)
setValue(`locales.${index}.description`, value)
}}
/>
</fieldset>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<FormLabel>Опис товару</FormLabel>
<JoditEditor
key={index + 5}
ref={editor}
config={config}
value={index === 0 ? content0 : content1}
className='mt-4 w-full'
onBlur={value => {
index === 0 ? setContent0(value) : setContent1(value)
setValue(`locales.${index}.content`, value)
}}
/>
</fieldset>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<FormLabel>Інструкція до товару</FormLabel>
<JoditEditor
key={index + 2125}
ref={editor}
config={config}
value={index === 0 ? instruction0 : instruction1}
className='mt-4 w-full'
onBlur={value => {
index === 0
? setInstruction0(value)
: setInstruction1(value)
setValue(`locales.${index}.instruction`, value)
}}
/>
</fieldset>
</TabsContent>
))}
{metaFields.map((_, index) => (
<TabsContent
id={`form-tab-${form.getValues(`locales.${index}.lang`)}`}
value={form.getValues(`locales.${index}.lang`)}
key={index}
className='space-y-4'
>
<fieldset className='rounded-lg border bg-gray-50 p-4'>
<legend className='rounded-lg border bg-gray-200 px-16 py-1 text-xl font-bold'>
META ДАНІ
</legend>
<FormField
control={form.control}
key={index + 'meta.title'}
name={`meta.${index}.title`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Назва</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 'meta.description'}
name={`meta.${index}.description`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Опис</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 'meta.keywords'}
name={`meta.${index}.keywords`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Ключові слова</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 'meta.author'}
name={`meta.${index}.author`}
render={({field}) => (
<FormItem className={'w-full'}>
<FormLabel>Автор</FormLabel>
<FormControl>
<Input type='text' placeholder='' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</fieldset>
</TabsContent>
))}
</Tabs>
</div>
<Button type='submit' className='!mt-0 w-full'>
Створити
</Button>
</form>
</Form>
)
}

View File

@@ -0,0 +1,169 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import fs from 'node:fs'
import path from 'node:path'
import {useCallback, useState} from 'react'
import Dropzone from 'react-dropzone'
import {useFieldArray, useForm} from 'react-hook-form'
import {z} from 'zod'
import {onProductCreateAction} from '@/actions/admin/product'
import {createCategoryFormSchema} from '@/lib/schemas/admin/category'
import {createProductFormSchema} from '@/lib/schemas/admin/product'
import {cn, dump} from '@/lib/utils'
import {ResourceMessages} from '@/types'
import {Button} from '@/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/ui/form'
import {Input} from '@/ui/input'
export default function CreateForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const form = useForm<z.infer<typeof createProductFormSchema>>({
resolver: zodResolver(createProductFormSchema),
mode: 'onBlur',
defaultValues: {
files: []
}
})
const {fields, append} = useFieldArray({
name: 'files',
control: form.control
})
console.log(form.formState.errors)
const onSubmit = async (values: z.infer<typeof createProductFormSchema>) => {
setLoading(true)
onProductCreateAction(values).then((res: any) => {
if (res?.error) {
setError(res?.error)
setSuccess('')
setLoading(false)
} else {
setSuccess(res?.success as string)
setError('')
setLoading(false)
}
})
}
return (
<div className='flex h-screen items-center justify-center'>
<Form {...form}>
<form
action=''
onSubmit={form.handleSubmit(onSubmit)}
className='max-w-md flex-1 space-y-5'
>
<div className='products_name_price_desc relative'>
{fields.map((_, index) => {
return (
<div key={index}>
<div className='mb-2 mt-7 text-xl font-bold'>
{/*{form.getValues(`files.${index}.file.name`)}*/}
{dump(form.getValues(`files.${index}`))}
</div>
<div className='flex gap-x-3'>
<FormField
control={form.control}
key={index}
name={`files.${index}.alt`}
render={({field}) => (
<FormItem>
<FormLabel>Файл Альт Ім'я</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage className='capitalize text-red-500' />
</FormItem>
)}
/>
<FormField
control={form.control}
key={index + 1}
name={`files.${index}.title`}
render={({field}) => (
<FormItem>
<FormLabel>Назва файлу</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage className='capitalize text-red-500' />
</FormItem>
)}
/>
</div>
</div>
)
})}
</div>
<div className='products relative'>
<FormField
control={form.control}
name='files'
render={() => (
<Dropzone
accept={{
'image/*': ['.jpg', '.jpeg', '.png']
}}
onDropAccepted={acceptedFiles => {
acceptedFiles.map(acceptedFile => {
console.log('acceptedFile', acceptedFile)
return append({
file: acceptedFile,
alt: '',
title: ''
})
})
}}
multiple={true}
maxSize={5000000}
>
{({getRootProps, getInputProps}) => (
<section>
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>
Перетягніть тут, киньте кілька файлів або натисніть,
щоб вибрати файли
</p>
</div>
</section>
)}
</Dropzone>
)}
/>
</div>
<Button type='submit' className='!mt-0 w-full'>
Створити
</Button>
</form>
</Form>
</div>
)
// return (
// <div className='flex flex-col bg-zinc-200 py-10'>
// <h1 className='text-center text-3xl font-bold capitalize'>
// Створити галерею
// </h1>
// <div className='mx-auto mb-10 mt-6 flex min-h-[320px] w-[80%] flex-wrap gap-1 rounded-md bg-white p-5 shadow-sm'></div>
// <div className='flex justify-center'>
// <Button>Завантажити зображення</Button>
// </div>
// </div>
// )
}

View File

@@ -0,0 +1,106 @@
import {
ChevronDown,
Home,
Inbox,
LayoutList,
Plus,
ScanBarcode,
Search,
Settings
} from 'lucide-react'
import AdminSidebarFooter from '@/components/(protected)/admin/footer'
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
} from '@/components/ui/sidebar'
import {ADMIN_DASHBOARD_PATH} from '@/lib/config/routes'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@/ui/collapsible'
const items = [
{
title: 'Головна',
url: `${ADMIN_DASHBOARD_PATH}`,
icon: Home
},
{
title: 'Категорії',
url: `${ADMIN_DASHBOARD_PATH}/category`,
icon: LayoutList
},
{
title: 'Товари',
url: `${ADMIN_DASHBOARD_PATH}/product`,
icon: ScanBarcode
}
// {
// title: 'Search',
// url: '#',
// icon: Search
// },
// {
// title: 'Settings',
// url: '#',
// icon: Settings
// }
]
export function AdminSidebar() {
return (
<Sidebar collapsible='icon' variant='sidebar'>
{/*<AdminSidebarHeader />*/}
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel asChild>Projects</SidebarGroupLabel>
<SidebarGroupAction title='Add Project'>
<Plus /> <span className='sr-only'>Add Project</span>
</SidebarGroupAction>
<SidebarGroupContent>SidebarGroupAction</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Application</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map(item => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<Collapsible defaultOpen className='group/collapsible'>
<SidebarGroup>
<SidebarGroupLabel asChild>
<CollapsibleTrigger>
Help
<ChevronDown className='ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180' />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>SidebarGroupContent</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
</SidebarContent>
<AdminSidebarFooter />
</Sidebar>
)
}

View File

@@ -0,0 +1,18 @@
import Logo from '@/components/shared/home/logo'
interface HeaderProps {
label: string
title: string
}
const AuthHeader = ({title, label}: HeaderProps) => {
return (
<div className='flex w-full flex-col items-center justify-center gap-y-4'>
<Logo />
<h1 className='text-3xl font-semibold'>{title}</h1>
<p className='text-sm text-muted-foreground'>{label}</p>
</div>
)
}
export default AuthHeader

View File

@@ -0,0 +1,16 @@
import Link from 'next/link'
import {Button} from '@/components/ui/button'
interface BackButtonProps {
label: string
href: string
}
export const BackButton = ({label, href}: BackButtonProps) => {
return (
<Button variant='link' className='w-full font-normal' size='sm' asChild>
<Link href={href}>{label}</Link>
</Button>
)
}

View File

@@ -0,0 +1,37 @@
import {ReactNode} from 'react'
import AuthHeader from './auth-header'
import {BackButton} from './back-button'
import {Card, CardContent, CardFooter, CardHeader} from '@/components/ui/card'
interface CardWrapperProps {
children: ReactNode
headerLabel: string
backButtonLabel: string
title: string
showSocial?: boolean
backButtonHref: string
}
const CardWrapper = ({
children,
headerLabel,
backButtonLabel,
backButtonHref,
title,
showSocial
}: CardWrapperProps) => {
return (
<Card className='m-auto shadow-md md:w-1/2 xl:w-1/4'>
<CardHeader>
<AuthHeader label={headerLabel} title={title} />
</CardHeader>
<CardContent>{children}</CardContent>
<CardFooter>
<BackButton label={backButtonLabel} href={backButtonHref} />
</CardFooter>
</Card>
)
}
export default CardWrapper

View File

@@ -0,0 +1,17 @@
import {ShieldAlert} from 'lucide-react'
interface FormSuccessProps {
message?: string
}
export const FormError = ({message}: FormSuccessProps) => {
if (!message) return null
return (
<div className='flex items-center space-x-4 rounded-lg bg-red-500/30 p-2 text-red-500'>
<ShieldAlert size={16} />
<p>{message}</p>
</div>
)
}
export default FormError

View File

@@ -0,0 +1,15 @@
import {CheckCheckIcon} from 'lucide-react'
interface FormSuccessProps {
message?: string
}
export const FormSuccess = ({message}: FormSuccessProps) => {
if (!message) return null
return (
<div className='flex items-center space-x-4 rounded-lg bg-emerald-500/30 p-2 text-emerald-500'>
<CheckCheckIcon className='h-4 w-4' />
<p>{message}</p>
</div>
)
}

View File

@@ -0,0 +1,104 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import {useState} from 'react'
import {useForm} from 'react-hook-form'
import {z} from 'zod'
import {login} from '@/actions/auth/login'
import CardWrapper from '@/components/auth/card-wrapper'
import {FormError} from '@/components/auth/form-error'
import GoogleLogin from '@/components/auth/google-login'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import {LoginSchema} from '@/lib/schemas'
import {Button} from '@/ui/button'
import {Input} from '@/ui/input'
export default function LoginForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: '',
password: ''
}
})
const onSubmit = async (data: z.infer<typeof LoginSchema>) => {
setLoading(true)
login(data).then(res => {
if (res?.error) {
setError(res?.error)
setLoading(false)
} else {
setError('')
setLoading(false)
}
})
}
return (
<CardWrapper
headerLabel='Create an account'
title='Register'
backButtonHref='/auth/login'
backButtonLabel='Already have an account'
showSocial
>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='m-auto space-y-6'
>
<div className='space-y-4'>
<FormField
control={form.control}
name='email'
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
{...field}
placeholder='johndoe@email.com'
type='email'
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({field}) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} placeholder='******' type='password' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormError message={error} />
<Button type='submit' className='w-full' disabled={loading}>
{loading ? 'Loading...' : 'Login'}
</Button>
</form>
</Form>
<GoogleLogin />
</CardWrapper>
)
}

View File

@@ -0,0 +1,133 @@
'use client'
import {zodResolver} from '@hookform/resolvers/zod'
import {useState} from 'react'
import {useForm} from 'react-hook-form'
import {z} from 'zod'
import {register} from '@/actions/auth/register'
import CardWrapper from '@/components/auth/card-wrapper'
import {FormError} from '@/components/auth/form-error'
import {FormSuccess} from '@/components/auth/form-success'
import GoogleLogin from '@/components/auth/google-login'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import {RegisterSchema} from '@/lib/schemas'
import {Button} from '@/ui/button'
import {Input} from '@/ui/input'
export default function RegisterForm() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const form = useForm<z.infer<typeof RegisterSchema>>({
resolver: zodResolver(RegisterSchema),
defaultValues: {
email: '',
name: '',
password: '',
passwordConfirmation: ''
}
})
const onSubmit = async (data: z.infer<typeof RegisterSchema>) => {
setLoading(true)
register(data).then(res => {
if (res.error) {
setError(res.error)
setLoading(false)
}
if (res.success) {
setError('')
setSuccess(res.success)
setLoading(false)
}
})
}
return (
<CardWrapper
headerLabel='Create an account'
title='Register'
backButtonHref='/auth/login'
backButtonLabel='Already have an account'
showSocial
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
<div className='space-y-4'>
<FormField
control={form.control}
name='email'
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
{...field}
placeholder='johndoe@email.com'
type='email'
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='name'
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} placeholder='John Doe' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({field}) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} placeholder='******' type='password' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='passwordConfirmation'
render={({field}) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input {...field} placeholder='******' type='password' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormSuccess message={success} />
<FormError message={error} />
<Button type='submit' className='w-full' disabled={loading}>
{loading ? 'Loading...' : 'Register'}
</Button>
</form>
</Form>
{/*<GoogleLogin />*/}
</CardWrapper>
)
}

View File

@@ -0,0 +1,14 @@
import {signOut} from '@/auth'
export function SignOutButton() {
return (
<form
action={async () => {
'use server'
await signOut()
}}
>
<button type='submit'>Sign Out</button>
</form>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import {useActionState} from 'react'
import {googleAuthenticate} from '@/actions/auth/google-login'
import GoogleIcon from '@/components/shared/icons/google'
import {Button} from '@/ui/button'
const GoogleLogin = () => {
const [errorMsgGoogle, dispatchGoogle] = useActionState(
googleAuthenticate,
undefined
) //googleAuthenticate hook
return (
<form className='mt-4 flex' action={dispatchGoogle}>
<Button
variant={'outline'}
className='flex w-full flex-row items-center gap-3'
>
<GoogleIcon />
Google Sign In
</Button>
<p>{errorMsgGoogle}</p>
</form>
)
}
export default GoogleLogin

View File

@@ -0,0 +1,60 @@
import {useTranslations} from 'next-intl'
import {SignOutButton} from '@/components/auth/forms/sign-out-button'
import CabinetButton from '@/components/shared/header/cabinet-button'
import {type SingedInSession} from '@/lib/permission'
import {dump} from '@/lib/utils'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@/ui/collapsible'
import {Separator} from '@/ui/separator'
export default function CabinetIndex({
slug,
session
}: {
slug: string[] | undefined
session: SingedInSession | null
}) {
const t = useTranslations('cabinet')
return (
<div className='my-8'>
<div className='container flex flex-col sm:flex-row'>
<section className='bw-layout-col-left bw-border-color border-r pt-3'>
<div className='flex items-center justify-between pr-4'>
<CabinetButton />
<div>
<p className='text-sm'>{session?.user?.name}</p>
<p className='text-xs'>{session?.user?.email}</p>
</div>
</div>
<Separator className='bw-separator-color my-4' />
<SignOutButton />
</section>
<div className='bw-layout-col-right pt-3'>
<section className='w-full'>
<h1 className='text-3xl font-extrabold'>
{t('personal-information.title')}
</h1>
<Separator className='my-4' />
{/*<BasicEditor placeholder={'type something'} />*/}
{/*<Separator className='my-4' />*/}
{slug ? <code>{slug[0]}</code> : <pre>{dump(session)}</pre>}
<Collapsible>
<CollapsibleTrigger>
Can I use this in my project?
</CollapsibleTrigger>
<CollapsibleContent>
Yes. Free to use for personal and commercial projects. No
attribution required.
</CollapsibleContent>
</Collapsible>
</section>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,19 @@
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #f05539;
}
.cls-3 {
fill: #b9c2e2;
}
.cls-4 {
fill: #a42f23;
}
.cls-5 {
fill: #ea5151;
}

View File

@@ -0,0 +1,115 @@
import styles from './css/fig-one.module.css'
const ar = 72.79 / 38.95
export default function FigOne() {
return (
<div>
<svg
id='fig-1'
data-name='fig 1'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 72.79 38.95'
>
<g id='Layer_1' data-name='Layer 1'>
<g>
<path d='M63.83,21.76c-.23,0-.47-.02-.71-.06-1.25-.2-2.3-.93-3.08-1.54-1.96-1.55-3.6-3.55-4.72-5.79-.06-.11-.01-.25,.1-.31,.11-.06,.25-.01,.31,.1,1.09,2.18,2.68,4.13,4.59,5.64,1.09,.86,1.97,1.31,2.87,1.45,1.07,.17,2.29-.12,3.46-.81,1.07-.63,1.98-1.52,2.87-2.37,.09-.09,.24-.09,.32,0,.09,.09,.09,.24,0,.32-.9,.87-1.84,1.77-2.95,2.43-.75,.44-1.83,.92-3.05,.92Z' />
<g>
<path
className='cls-1'
d='M71.22,16.24c-1.18-.57-2.6-.08-3.17,1.11-.57,1.18-.08,2.6,1.11,3.17,1.18,.57,2.6,.08,3.17-1.11,.57-1.18,.08-2.6-1.11-3.17Z'
/>
<path d='M70.18,20.99c-.38,0-.77-.08-1.13-.26-1.29-.63-1.84-2.19-1.21-3.48,.3-.63,.83-1.1,1.49-1.33,.66-.23,1.36-.19,1.99,.11,.63,.3,1.1,.83,1.33,1.49,.23,.66,.19,1.36-.11,1.99-.45,.93-1.38,1.47-2.35,1.47Zm0-4.75c-.24,0-.48,.04-.71,.12-.54,.19-.98,.58-1.23,1.09-.52,1.07-.07,2.35,1,2.87,1.07,.52,2.35,.07,2.87-1,.25-.52,.28-1.1,.09-1.64-.19-.54-.58-.98-1.09-1.23h0c-.3-.14-.61-.22-.93-.22Z' />
</g>
<path d='M6.35,23.78c-1.23,0-2.55-.78-3.91-2.33-.08-.09-.07-.24,.02-.32,.09-.08,.24-.07,.32,.02,1.55,1.76,2.99,2.45,4.27,2.07,1.97-.59,3.5-3.72,4.2-8.6,.02-.13,.13-.21,.26-.19,.12,.02,.21,.13,.19,.26-.73,5.06-2.38,8.33-4.53,8.97-.27,.08-.55,.12-.83,.12Z' />
<g>
<path
className='cls-1'
d='M3.64,19.16c-1.18-.57-2.6-.08-3.17,1.11-.57,1.18-.08,2.6,1.11,3.17,1.18,.57,2.6,.08,3.17-1.11,.57-1.18,.08-2.6-1.11-3.17Z'
/>
<path d='M2.6,23.91c-.39,0-.77-.09-1.13-.26-.63-.3-1.1-.83-1.33-1.49-.23-.66-.19-1.36,.11-1.99,.62-1.29,2.19-1.84,3.48-1.21h0c.63,.3,1.1,.83,1.33,1.49,.23,.66,.19,1.36-.11,1.99-.3,.63-.83,1.1-1.49,1.33-.28,.1-.57,.15-.86,.15Zm0-4.75c-.8,0-1.57,.45-1.94,1.21-.25,.52-.28,1.1-.09,1.64,.19,.54,.58,.98,1.09,1.23,.52,.25,1.1,.28,1.64,.09,.54-.19,.98-.58,1.23-1.09,.25-.52,.28-1.1,.09-1.64-.19-.54-.58-.98-1.09-1.23h0c-.3-.15-.62-.21-.93-.21Z' />
</g>
<g>
<path d='M28.61,36.94s0,0,0,0c-.13,0-.23-.11-.22-.24,.14-4.32,.09-8.7-.16-13.01,0-.13,.09-.23,.21-.24,.12,0,.23,.09,.24,.21,.25,4.33,.31,8.72,.16,13.05,0,.12-.11,.22-.23,.22Z' />
<path d='M28.61,36.94s0,0,0,0c-.13,0-.23-.11-.22-.24,.14-4.32,.09-8.7-.16-13.01,0-.13,.09-.23,.21-.24,.12,0,.23,.09,.24,.21,.25,4.33,.31,8.72,.16,13.05,0,.12-.11,.22-.23,.22Z' />
<path d='M42.12,36.94c-.12,0-.22-.1-.23-.22-.14-4.33-.09-8.72,.16-13.05,0-.13,.12-.22,.24-.21,.13,0,.22,.12,.21,.24-.25,4.31-.31,8.69-.16,13.01,0,.13-.09,.23-.22,.24,0,0,0,0,0,0Z' />
<g>
<path
className='cls-1'
d='M26.88,35.49c-.94,.1-1.94,.22-2.59,1-.32,.39-.44,.93-.24,1.39,.31,.72,1.12,.82,1.83,.83,1.07,.02,2.07,.02,3.12-.21,.64-.14,1.02-.52,1.09-2,.02-.53-.39-.99-.93-1.01-.78-.03-1.83-.06-2.29,0Z'
/>
<path d='M26.6,38.95c-.24,0-.48,0-.72,0-.81-.02-1.68-.14-2.04-.97-.23-.52-.12-1.15,.27-1.63,.72-.87,1.84-.99,2.74-1.09h0c.47-.05,1.52-.02,2.32,0,.32,.01,.62,.15,.83,.39,.22,.24,.33,.54,.31,.86-.06,1.43-.41,2.03-1.27,2.22-.84,.18-1.64,.22-2.45,.22Zm.3-3.24c-.86,.09-1.83,.19-2.43,.92-.29,.35-.36,.79-.21,1.16,.25,.59,.93,.68,1.63,.69,1.02,.02,2.03,.02,3.07-.21,.46-.1,.84-.3,.91-1.79,0-.2-.06-.38-.19-.53-.13-.15-.32-.23-.51-.24-1.11-.04-1.91-.05-2.25,0h0Zm-.02-.23h0Z' />
</g>
<g>
<path
className='cls-1'
d='M43.85,35.49c.94,.1,1.94,.22,2.59,1,.32,.39,.44,.93,.24,1.39-.31,.72-1.12,.82-1.83,.83-1.07,.02-2.07,.02-3.12-.21-.64-.14-1.02-.52-1.09-2-.02-.53,.39-.99,.93-1.01,.78-.03,1.83-.06,2.29,0Z'
/>
<path d='M44.12,38.95c-.81,0-1.61-.04-2.45-.22-.86-.19-1.2-.79-1.27-2.22-.01-.32,.1-.62,.31-.86,.22-.24,.51-.37,.83-.39,.8-.03,1.85-.06,2.32,0h0c.9,.09,2.02,.21,2.74,1.09,.39,.48,.5,1.1,.27,1.63-.36,.83-1.23,.95-2.04,.97-.24,0-.48,0-.72,0Zm-1.05-3.26c-.4,0-.91,.01-1.5,.03-.2,0-.38,.09-.51,.24-.13,.15-.2,.33-.19,.53,.07,1.49,.45,1.69,.91,1.79,1.04,.23,2.04,.23,3.07,.21,.7-.01,1.37-.11,1.63-.69,.16-.37,.08-.81-.21-1.16-.6-.73-1.57-.83-2.43-.92h0c-.16-.02-.41-.03-.75-.03Z' />
</g>
</g>
<g>
<path d='M58.22,11.86c-.12-1.22-.31-2.86-.87-3.96-.88-1.74-2.08-2.55-3.92-3.21-1.84-.67-4.34-.86-6.28-1.05-2.7-.26-9.2-.48-12.34-.48s-9.64,.22-12.34,.48c-1.95,.19-4.44,.39-6.28,1.05-1.84,.67-3.03,1.47-3.92,3.21-.56,1.1-.75,2.74-.87,3.96-.12,1.22-.16,2.45-.13,3.67-.03,1.22,.01,2.45,.13,3.67,.12,1.22,.31,2.86,.87,3.96,.88,1.74,2.08,2.55,3.92,3.21,1.84,.67,4.34,.86,6.28,1.05,2.7,.26,8.18,.48,12.34,.48s9.64-.22,12.34-.48c1.95-.19,4.44-.39,6.28-1.05,1.84-.67,3.03-1.47,3.92-3.21,.56-1.1,.75-2.73,.87-3.96,.12-1.22,.16-2.45,.13-3.67,.03-1.22-.01-2.45-.13-3.67Z' />
<path d='M34.81,28.15c-4.1,0-9.65-.22-12.36-.48l-.49-.05c-1.84-.18-4.14-.4-5.85-1.02-2.4-.87-3.35-1.96-4.05-3.33-.55-1.09-.76-2.66-.9-4.05-.12-1.21-.16-2.45-.13-3.7-.03-1.24,.02-2.48,.13-3.69,.13-1.39,.34-2.96,.9-4.05,.96-1.9,2.32-2.7,4.05-3.33,1.72-.62,4.01-.84,5.85-1.02l.49-.05c2.6-.25,9.08-.48,12.36-.48s9.76,.23,12.36,.48l.49,.05c1.84,.18,4.14,.4,5.85,1.02,2.4,.87,3.35,1.96,4.05,3.33,.55,1.09,.76,2.66,.9,4.05,.12,1.21,.16,2.45,.13,3.7,.03,1.24-.02,2.48-.13,3.69-.13,1.39-.34,2.96-.9,4.05-.96,1.9-2.32,2.7-4.05,3.33-1.72,.62-4.01,.84-5.85,1.02l-.49,.05c-2.71,.27-8.26,.48-12.36,.48Zm0-24.75c-3.27,0-9.72,.23-12.32,.48l-.49,.05c-1.82,.17-4.08,.39-5.73,.99-1.67,.61-2.89,1.34-3.78,3.1-.52,1.02-.72,2.53-.84,3.87-.11,1.19-.16,2.41-.13,3.64-.03,1.24,.02,2.46,.13,3.65,.13,1.34,.33,2.86,.84,3.87,.89,1.76,2.11,2.49,3.78,3.1,1.66,.6,3.92,.82,5.73,.99l.49,.05c2.7,.26,8.23,.48,12.32,.48s9.61-.21,12.32-.48l.49-.05c1.82-.17,4.08-.39,5.73-.99,1.67-.61,2.89-1.34,3.78-3.1,.52-1.02,.72-2.53,.84-3.87,.11-1.19,.16-2.41,.13-3.64,.03-1.24-.02-2.46-.13-3.65h0c-.13-1.34-.33-2.86-.84-3.87-.89-1.76-2.11-2.49-3.78-3.1-1.66-.6-3.92-.82-5.73-.99l-.49-.05c-2.59-.25-9.05-.48-12.32-.48Z' />
</g>
<g>
<path
className='cls-3'
d='M58.22,8.95c-.12-1.22-.31-2.86-.87-3.96-.88-1.74-2.08-2.55-3.92-3.21-1.84-.67-4.34-.86-6.28-1.05-2.7-.26-9.2-.48-12.34-.48s-9.64,.22-12.34,.48c-1.95,.19-4.44,.39-6.28,1.05-1.84,.67-3.03,1.47-3.92,3.21-.56,1.1-.75,2.74-.87,3.96-.12,1.22-.16,2.45-.13,3.67-.03,1.22,.01,2.45,.13,3.67,.12,1.22,.31,2.86,.87,3.96,.88,1.74,2.08,2.55,3.92,3.21,1.84,.67,4.34,.86,6.28,1.05,2.7,.26,8.18,.48,12.34,.48s9.64-.22,12.34-.48c1.95-.19,4.44-.39,6.28-1.05,1.84-.67,3.03-1.47,3.92-3.21,.56-1.1,.75-2.74,.87-3.96,.12-1.22,.16-2.45,.13-3.67,.03-1.22-.01-2.45-.13-3.67Z'
/>
<path d='M34.81,25.25c-4.1,0-9.65-.22-12.36-.48l-.49-.05c-1.84-.18-4.14-.4-5.85-1.02-2.4-.87-3.35-1.96-4.05-3.33-.55-1.09-.76-2.66-.9-4.05-.12-1.21-.16-2.45-.13-3.7-.03-1.24,.02-2.48,.13-3.69,.13-1.39,.34-2.96,.9-4.05,.96-1.9,2.32-2.7,4.05-3.33,1.72-.62,4.01-.84,5.85-1.02l.49-.05c2.6-.25,9.08-.48,12.36-.48s9.76,.23,12.36,.48l.49,.05c1.84,.18,4.14,.4,5.85,1.02,2.4,.87,3.35,1.96,4.05,3.33,.55,1.09,.76,2.66,.9,4.05,.12,1.21,.16,2.45,.13,3.7,.03,1.24-.02,2.48-.13,3.69-.13,1.39-.34,2.96-.9,4.05-.96,1.9-2.32,2.7-4.05,3.33-1.72,.62-4.01,.84-5.86,1.02l-.49,.05c-2.71,.27-8.26,.48-12.36,.48ZM34.81,.49c-3.27,0-9.72,.23-12.32,.48l-.49,.05c-1.82,.17-4.07,.39-5.73,.99-1.67,.61-2.89,1.34-3.78,3.1-.52,1.02-.72,2.53-.84,3.87-.11,1.19-.16,2.41-.13,3.64-.03,1.24,.02,2.46,.13,3.65,.13,1.34,.33,2.86,.84,3.87,.89,1.76,2.11,2.49,3.78,3.1,1.66,.6,3.92,.82,5.73,.99l.49,.05c2.7,.26,8.23,.48,12.32,.48s9.61-.21,12.32-.48l.49-.05c1.82-.17,4.08-.39,5.73-.99,1.67-.61,2.89-1.34,3.78-3.1,.52-1.02,.72-2.53,.84-3.87,.11-1.19,.16-2.41,.13-3.64,.03-1.24-.02-2.46-.13-3.65h0c-.13-1.34-.33-2.86-.84-3.87-.89-1.76-2.11-2.49-3.78-3.1-1.66-.6-3.92-.82-5.73-.99l-.49-.05c-2.59-.25-9.05-.48-12.32-.48Z' />
</g>
<path d='M34.81,25.25c-.14,0-.25-.11-.25-.25V.25c0-.14,.11-.25,.25-.25s.25,.11,.25,.25V25c0,.14-.11,.25-.25,.25Z' />
<g>
<path
className='cls-4'
d='M30.17,12.96c-1.08,0-1.65,1.28-.91,2.07,.5,.54,1.05,1.04,1.66,1.49,1.34,.99,3.05,1.59,4.7,1.3,1.11-.2,2.12-.8,3.02-1.47,.51-.38,.99-.79,1.45-1.23,.81-.77,.26-2.13-.85-2.13-2.69,.01-6.52-.01-9.07-.03Z'
/>
<path d='M34.76,18.12c-1.32,0-2.73-.49-3.98-1.42-.6-.45-1.16-.94-1.7-1.52-.4-.43-.51-1.04-.27-1.58,.24-.55,.75-.88,1.35-.88h0c3.24,.02,6.59,.04,9.07,.03h0c.61,0,1.13,.35,1.35,.92,.23,.57,.1,1.18-.35,1.61-.47,.44-.96,.86-1.47,1.25-1.14,.85-2.13,1.34-3.12,1.52-.29,.05-.59,.08-.9,.08Zm-4.6-4.93c-.48,0-.79,.31-.92,.6-.13,.29-.15,.73,.18,1.08,.52,.55,1.05,1.03,1.63,1.46,1.43,1.06,3.08,1.52,4.52,1.25,.92-.17,1.85-.62,2.93-1.43,.5-.37,.98-.78,1.43-1.21,.36-.35,.36-.8,.24-1.1-.12-.3-.43-.63-.93-.63h0c-2.49,.01-5.83,0-9.07-.03h0Z' />
</g>
<g>
<path
className='cls-5'
d='M36.02,16.31c-.64-.17-1.31-.24-1.97-.2-.66,.04-1.32,.2-1.92,.47-.21,.1-.42,.21-.61,.33,1.24,.75,2.7,1.16,4.11,.91,.71-.13,1.38-.42,2.01-.79-.5-.33-1.04-.57-1.61-.72Z'
/>
<path d='M34.76,18.12c-1.11,0-2.28-.35-3.37-1.01-.07-.04-.11-.11-.11-.19,0-.08,.04-.15,.11-.2,.21-.13,.42-.25,.64-.35,.62-.28,1.29-.45,2-.49,.69-.05,1.38,.02,2.05,.21h0c.6,.16,1.17,.42,1.68,.75,.07,.04,.11,.12,.1,.2,0,.08-.04,.15-.11,.19-.72,.43-1.41,.69-2.08,.82-.29,.05-.59,.08-.9,.08Zm-2.8-1.21c1.2,.65,2.47,.89,3.62,.68,.52-.09,1.04-.28,1.6-.57-.38-.21-.79-.38-1.22-.49h0c-.62-.17-1.26-.23-1.9-.19-.66,.04-1.28,.2-1.84,.45-.08,.04-.17,.08-.25,.12Z' />
</g>
<g>
<path
className='cls-1'
d='M30.76,14.09c.33,.14,2.82,.36,3.93,.36,.7,0,3.37-.09,3.97-.25,.32-.08,1.11-.83,1.2-1.04-.18-.1-.38-.16-.61-.16-2.69,.01-6.52-.01-9.07-.03-.12,0-.24,.02-.35,.05,.05,.16,.64,.95,.94,1.08Z'
/>
<path d='M34.66,14.67c-1.06,0-3.6-.2-3.99-.37h0c-.38-.17-1-1.01-1.07-1.22-.02-.06-.01-.12,.02-.18s.08-.09,.14-.11c.13-.04,.28-.05,.41-.06,3.24,.02,6.59,.04,9.07,.03,.26,0,.51,.06,.73,.19,.1,.06,.14,.18,.1,.28-.11,.27-.96,1.08-1.35,1.18-.64,.17-3.39,.26-4.03,.26-.01,0-.02,0-.03,0Zm-4.46-1.48c.19,.26,.5,.62,.65,.68h0c.26,.11,2.67,.34,3.84,.34,.73,0,3.36-.1,3.91-.24,.18-.05,.65-.46,.9-.72-.08-.02-.17-.03-.25-.03-2.48,.01-5.81,0-9.05-.03Z' />
</g>
<path
className='cls-1'
d='M55.06,10.09c-.39,0-.75-.25-.87-.64-.6-1.93-2.15-3.59-4.03-4.33-.47-.18-.7-.72-.52-1.19,.18-.47,.71-.7,1.19-.52,2.43,.95,4.34,3,5.11,5.49,.15,.48-.12,1-.6,1.15-.09,.03-.18,.04-.27,.04Z'
/>
<g>
<path d='M40.98,10.04c0-1.04,.53-1.56,1.18-1.56s1.18,.52,1.18,1.56-.53,1.56-1.18,1.56-1.18-.52-1.18-1.56' />
<path d='M26.28,10.04c0-1.04,.53-1.56,1.18-1.56s1.18,.52,1.18,1.56-.53,1.56-1.18,1.56-1.18-.52-1.18-1.56' />
</g>
<g>
<path
className='cls-2'
d='M24.91,13.48c0,1.16-.94,2.11-2.11,2.11s-2.11-.94-2.11-2.11,.94-2.11,2.11-2.11,2.11,.94,2.11,2.11Z'
/>
<path
className='cls-2'
d='M48.92,13.48c0,1.16-.94,2.11-2.11,2.11s-2.11-.94-2.11-2.11,.94-2.11,2.11-2.11,2.11,.94,2.11,2.11Z'
/>
</g>
<path
className='cls-1'
d='M15.38,20.59c-.33,0-.65-.18-.81-.49-.48-.92-.78-1.96-.85-2.99-.04-.5,.34-.94,.85-.98,.5-.04,.94,.34,.98,.85,.06,.79,.28,1.58,.65,2.28,.23,.45,.06,1-.39,1.23-.14,.07-.28,.1-.42,.1Z'
/>
<path
className='cls-1'
d='M18.01,22.51c-.14,0-.28-.03-.41-.1l-.48-.24c-.45-.23-.64-.78-.41-1.23,.23-.45,.78-.64,1.23-.41l.48,.24c.45,.23,.64,.78,.41,1.23-.16,.32-.48,.51-.82,.51Z'
/>
</g>
</g>
</svg>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function FigThree() {
return <></>
}

View File

@@ -0,0 +1,3 @@
export default function FigTwo() {
return <div>Fig1</div>
}

View File

@@ -0,0 +1,47 @@
'use client'
import {useTranslations} from 'next-intl'
import Image from 'next/image'
import Fig1 from '@/public/images/above/fig-1.svg'
import Fig2 from '@/public/images/above/fig-2.svg'
import Fig3 from '@/public/images/above/fig-3.svg'
export default function Above() {
const t = useTranslations('Banner.Above')
return (
<div className='flex w-full items-end justify-center bg-brand-violet md:min-h-[43px] xl:min-h-[51px]'>
<div className='mx-0 mb-0.5'>
<Image
width={72.79}
height={38.95}
src={Fig1}
alt=''
className='max-h-[35px]'
/>
</div>
<div className='mx-0'>
<Image
width={80.21}
height={43.78}
src={Fig2}
alt=''
className='max-h-[41px]'
/>
</div>
<div className='self-center font-medium text-brand-yellow'>
{t('title')}
</div>
<div className='mx-1'>
<Image
width={98.89}
height={47.27}
src={Fig3}
alt=''
className='max-h-[37px]'
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,175 @@
'use client'
import {
AudioWaveform,
BookOpen,
Bot,
Command,
Frame,
GalleryVerticalEnd,
Map,
PieChart,
Settings2,
SquareTerminal
} from 'lucide-react'
import * as React from 'react'
import {NavMain} from '@/components/shared/nav-main'
import {NavProjects} from '@/components/shared/nav-projects'
import {NavUser} from '@/components/shared/nav-user'
import {TeamSwitcher} from '@/components/shared/team-switcher'
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail
} from '@/components/ui/sidebar'
// This is sample data.
const data = {
user: {
name: 'shadcn',
email: 'm@example.com',
avatar: '/avatars/shadcn.jpg'
},
teams: [
{
name: 'Acme Inc',
logo: GalleryVerticalEnd,
plan: 'Enterprise'
},
{
name: 'Acme Corp.',
logo: AudioWaveform,
plan: 'Startup'
},
{
name: 'Evil Corp.',
logo: Command,
plan: 'Free'
}
],
navMain: [
{
title: 'Playground',
url: '#',
icon: SquareTerminal,
isActive: true,
items: [
{
title: 'History',
url: '#'
},
{
title: 'Starred',
url: '#'
},
{
title: 'Settings',
url: '#'
}
]
},
{
title: 'Models',
url: '#',
icon: Bot,
items: [
{
title: 'Genesis',
url: '#'
},
{
title: 'Explorer',
url: '#'
},
{
title: 'Quantum',
url: '#'
}
]
},
{
title: 'Documentation',
url: '#',
icon: BookOpen,
items: [
{
title: 'Introduction',
url: '#'
},
{
title: 'Get Started',
url: '#'
},
{
title: 'Tutorials',
url: '#'
},
{
title: 'Changelog',
url: '#'
}
]
},
{
title: 'Settings',
url: '#',
icon: Settings2,
items: [
{
title: 'General',
url: '#'
},
{
title: 'Team',
url: '#'
},
{
title: 'Billing',
url: '#'
},
{
title: 'Limits',
url: '#'
}
]
}
],
projects: [
{
name: 'Design Engineering',
url: '#',
icon: Frame
},
{
name: 'Sales & Marketing',
url: '#',
icon: PieChart
},
{
name: 'Travel',
url: '#',
icon: Map
}
]
}
export function AppSidebar({...props}: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}

View File

@@ -0,0 +1,56 @@
'use client'
// https://xdsoft.net/jodit/play.html?spellcheck=true&language=ua&direction=ltr&saveHeightInStorage=true&saveModeInStorage=true&defaultActionOnPaste=insert_as_text&disablePlugins=ai-assistant%2Cmobile%2Cprint%2Cspeech-recognize%2Ctable%2Ctable-keyboard-navigation%2Cpowered-by-jodit%2Ciframe&minHeight=360&maxHeight=NaN&maxWidth=NaN&buttons=bold%2Citalic%2Cunderline%2Cstrikethrough%2Ceraser%2Cul%2Col%2Cfont%2Cfontsize%2Cparagraph%2ClineHeight%2Csuperscript%2Csubscript%2CclassSpan%2Cfile%2Cimage%2Cvideo%2Cspellcheck%2Ccut
//import JoditEditor from 'jodit-react'
import dynamic from 'next/dynamic'
import React, {useMemo, useRef, useState} from 'react'
const JoditEditor = dynamic(() => import('jodit-react'), {ssr: false})
export default function BasicEditor({
placeholder,
maxHeight,
handleChange
}: {
placeholder: string
maxHeight: number
handleChange: any
}) {
const editor = useRef(null)
const [content, setContent] = useState('')
const config = useMemo(
() => ({
readonly: false, // all options from https://xdsoft.net/jodit/docs/,
placeholder: placeholder || 'Start typings...',
spellcheck: true,
language: 'ua',
saveHeightInStorage: true,
saveModeInStorage: true,
//defaultActionOnPaste: 'insert_as_text',
disablePlugins:
'ai-assistant,mobile,print,speech-recognize,table,table-keyboard-navigation,powered-by-jodit,iframe',
minHeight: maxHeight || 320,
maxHeight: 1100,
// maxWidth: 992,
uploader: {
insertImageAsBase64URI: true,
imagesExtensions: ['jpg', 'png', 'jpeg', 'gif', 'svg', 'webp']
},
buttons:
'bold,italic,underline,strikethrough,eraser,ul,ol,font,fontsize,paragraph,lineHeight,superscript,subscript,classSpan,file,image,video,spellcheck,cut'
}),
[placeholder, maxHeight]
)
return (
<JoditEditor
ref={editor}
value={content}
config={config}
tabIndex={1} // tabIndex of textarea
onBlur={newContent => setContent(newContent)} // preferred to use only this option to update the content for performance reasons
onChange={handleChange}
/>
)
}

View File

@@ -0,0 +1,28 @@
'use client'
// 31:47
import {ChevronUp} from 'lucide-react'
import SocialMediaPanel from '@/components/shared/social-media-panel'
import {Button} from '@/ui/button'
export default function Footer() {
return (
<footer className='bg-brand-violet w-full py-3 text-white'>
<div className='container flex items-center justify-between'>
<div>Політика конфіденційності</div>
<div>Договір оферти</div>
<div>Доставка і повернення</div>
<div>Контакти</div>
<SocialMediaPanel color='#fff' />
<Button
variant='ghost'
className='bg-brand-violet rounded-none'
onClick={() => window.scrollTo({top: 0, behavior: 'smooth'})}
>
<ChevronUp className='mr-2 h-4 w-4' />
</Button>
</div>
</footer>
)
}

View File

@@ -0,0 +1,33 @@
import {CircleUserRound} from 'lucide-react'
import Link from 'next/link'
import {auth} from '@/auth'
import {avatarFallback} from '@/lib/utils'
import {Avatar, AvatarFallback, AvatarImage} from '@/ui/avatar'
export default async function CabinetButton() {
const session = await auth()
return (
<Link href='/cabinet' className='header-button' aria-label='Кабінет'>
<button className='flex flex-col items-center' role='button'>
{session ? (
<>
<Avatar className='hs1-[21px] w1-[21px] border2-2 border2-brand-violet'>
<AvatarImage src={session.user?.image as string} alt='avatar' />
<AvatarFallback className='text-xs'>
{avatarFallback(session.user?.name as string)}
</AvatarFallback>
</Avatar>
</>
) : (
<>
<CircleUserRound className='h-[21px] w-[21px]' />
GA4_Ecommerce_View_Item_List_Trigger
</>
)}
{/*<span className='text-sm'>Кабінет</span>*/}
</button>
</Link>
)
}

View File

@@ -0,0 +1,34 @@
import {Heart, ShoppingCartIcon, UserIcon} from 'lucide-react'
import {useTranslations} from 'next-intl'
import CabinetButton from '@/components/shared/header/cabinet-button'
import {Link} from '@/i18n/routing'
export default function HeaderControls() {
const t = useTranslations('cart')
return (
<div className='flex w-full justify-end gap-x-9 text-brand-violet'>
<CabinetButton />
<Link href={'#' as never} className='header-button' aria-label='Вибране'>
<button className='flex flex-col items-center' role='button'>
<Heart className='h-[21px] w-[21px]' />
<span className='font1-bold text-sm'>{t('favorites')}</span>
</button>
</Link>
<Link
href={'/checkout' as never}
className='header-button'
aria-label='Кошик'
>
<button className='flex flex-col items-center' role='button'>
<ShoppingCartIcon className='h-[21px] w-[21px]' />
<span className='font1-bold text-sm'>{t('basket')}</span>
</button>
</Link>
</div>
)
}

View File

@@ -0,0 +1,54 @@
import {Menu} from '@radix-ui/react-menu'
import HeaderControls from './controls'
import Logo from '@/components/shared/home/logo'
import LocaleSwitcher from '@/components/shared/locale-switcher'
import Navbar from '@/components/shared/navbar'
import SearchForm from '@/components/shared/search/form'
import SocialMediaPanel from '@/components/shared/social-media-panel'
import {Switch} from '@/ui/switch'
type MenuProps = {
name: string
slug: string
href: string
}
export default async function Header() {
/*{
searchParams
}: {
searchParams: Promise<{query?: string}>
}*/
//const query = (await searchParams).query
return (
<header className='w-full border-none bg-background text-white'>
<div className='container flex'>
<div className='bw-layout-col-left'>
<Logo />
</div>
<div className='bw-layout-col-right flex-col'>
<div className='mt-1.5 flex h-10 items-center'>
<div className='bw-header-col-left flex justify-between gap-x-10'>
<Navbar />
<SocialMediaPanel size={16} className='gap-x-3 pr-1' />
</div>
<div className='bw-header-col-right flex justify-end text-stone'>
<LocaleSwitcher />
</div>
</div>
<div className='flex items-center'>
<div className='bw-header-col-left'>
<SearchForm />
</div>
<div className='bw-header-col-right'>
<HeaderControls />
</div>
</div>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,44 @@
import Image from 'next/image'
import * as React from 'react'
import {cards} from '@/lib/data'
import {Card, CardContent} from '@/ui/card'
import {Carousel, CarouselContent, CarouselItem} from '@/ui/carousel'
export default function FeatureCards() {
return (
<Carousel className='mx-auto w-full'>
<CarouselContent className='-ml-2 md:-ml-4'>
{cards.map((card: any) => (
<CarouselItem
key={card.title}
className='pl-3 md:basis-1/3 lg:basis-1/4 xl:basis-1/5'
>
<div className='p-1'>
<Card className='border-[2px] border-brand-violet'>
<CardContent className='aspect-card flex items-center justify-center p-1'>
<CarouselItem>
<Image
src={card.image}
width={256}
height={256}
className='object-scale-down'
priority
alt={''}
/>
</CarouselItem>
</CardContent>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
/*<div className='container grid grid-cols-1 md:grid-cols-2 md:gap-44 lg:grid-cols-4'>
{cards.map((card: any) => (
<ProductCard card={card} key={card.title} />
))}
</div>*/
)
}

View File

@@ -0,0 +1,74 @@
'use client'
import Autoplay from 'embla-carousel-autoplay'
import Image from 'next/image'
import Link from 'next/link'
import * as React from 'react'
import {Button} from '@/components/ui/button'
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious
} from '@/components/ui/carousel'
import {cn} from '@/lib/utils'
export function HomeCarousel({
items
}: {
items: {
image: string
url: string
title: string
buttonCaption: string
}[]
}) {
const plugin = React.useRef(Autoplay({delay: 5000, stopOnInteraction: true}))
return (
<Carousel
dir='ltr'
plugins={[plugin.current]}
className='mx-auto w-full'
onMouseEnter={plugin.current.stop}
onMouseLeave={plugin.current.reset}
>
<CarouselContent>
{items.map(item => (
<CarouselItem key={item.title}>
<Link href={item.url}>
<div className='relative -m-0.5 flex aspect-univisium items-center justify-center'>
<Image
src={item.image}
alt={item.title}
fill
className='object-cover'
priority
/>
<div className='absolute left-16 top-1/2 hidden w-1/3 -translate-y-1/2 transform md:left-32'>
<h2
className={cn(
'mb-4 text-lg font-bold text-primary shadow-brand-violet drop-shadow-xl md:text-6xl'
)}
>
{`${item.title}`}
</h2>
<Button className='hidden md:block'>
{`${item.buttonCaption}`}
</Button>
</div>
</div>
</Link>
</CarouselItem>
))}
</CarouselContent>
{/*<CarouselPrevious className='left-0 h-[78px] w-[78px] text-6xl md:left-12' />
<CarouselNext className='right-0 h-[78px] w-[78px] md:right-12' />*/}
<CarouselPrevious className='absolute left-[1rem] top-1/2 z-10 h-[78px] w-[78px] -translate-y-1/2 transform' />
<CarouselNext className='absolute right-[1rem] top-1/2 z-10 h-[78px] w-[78px] -translate-y-1/2 transform' />
</Carousel>
)
}

View File

@@ -0,0 +1,29 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import {APP_NAME} from '@/lib/constants'
import logoImg from '@/public/images/logo.svg'
export default function Logo() {
const ar = 121 / 192
const w = 112
return (
<div className='mt-0.5 flex items-center justify-center'>
<Link
href='/'
className='m-1 flex cursor-pointer items-center pt-[7px] text-2xl font-extrabold outline-0'
>
<Image
src={logoImg}
width={w}
height={w * ar}
alt={`${APP_NAME} logo`}
className='w-[131]'
/>
</Link>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import React from 'react'
import IconProps from '@/components/shared/icons/props'
import {cn} from '@/lib/utils'
export default function GoogleIcon({className, size, color}: IconProps) {
return (
<svg
width={size || 24}
height={size || 24}
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
className={cn('bw-icon', className)}
>
<path
d='M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z'
fill='#4285F4'
/>
<path
d='M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z'
fill='#34A853'
/>
<path
d='M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z'
fill='#FBBC05'
/>
<path
d='M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z'
fill='#EA4335'
/>
<path d='M1 1h22v22H1z' fill='none' />
</svg>
)
}

View File

@@ -0,0 +1,5 @@
export default interface IconProps {
className?: string
size?: string | number
color?: string
}

View File

@@ -0,0 +1,22 @@
import IconProps from '@/components/shared/icons/props'
import {BRAND_ICON_COLOR} from '@/lib/constants'
import {cn} from '@/lib/utils'
export default function WhatsappIcon({className, size, color}: IconProps) {
return (
<svg
width={size || 24}
height={size || 24}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
{/*https://lineicons.com/icons?q=goo*/}
<path
className={cn('bw-icon', className)}
d='M19.074 4.89389C17.2091 3.02894 14.6689 2 12.0644 2C6.59814 2 2.12869 6.4373 2.12869 11.9035C2.12869 13.672 2.57885 15.3441 3.44702 16.8875L2.03223 22L7.33769 20.6495C8.78464 21.4212 10.4245 21.8714 12.0965 21.8714C17.5306 21.8392 21.9679 17.4019 21.9679 11.9035C21.9679 9.26688 20.939 6.791 19.074 4.89389ZM12.0322 20.1672C10.5853 20.1672 9.07403 19.7492 7.82001 18.9775L7.49846 18.7846L4.37949 19.5884L5.24766 16.5659L5.05473 16.2444C4.25088 14.926 3.80072 13.3826 3.80072 11.8392C3.80072 7.30547 7.46631 3.63987 12.0322 3.63987C14.2187 3.63987 16.2766 4.50804 17.82 6.05145C19.3634 7.59486 20.2316 9.68489 20.2316 11.9035C20.2959 16.5016 16.566 20.1672 12.0322 20.1672ZM16.566 13.9936C16.3088 13.865 15.119 13.254 14.8297 13.2219C14.6046 13.1254 14.4116 13.0932 14.283 13.3505C14.1544 13.6077 13.6399 14.1222 13.5113 14.3151C13.3827 14.4437 13.2541 14.508 12.9647 14.3473C12.7075 14.2187 11.9358 13.9936 10.9711 13.0932C10.2316 12.4502 9.71711 11.6463 9.62065 11.3569C9.49203 11.0997 9.5885 11.0032 9.74927 10.8424C9.87788 10.7138 10.0065 10.5852 10.103 10.3923C10.2316 10.2637 10.2316 10.135 10.3602 9.97428C10.4888 9.84566 10.3924 9.65274 10.328 9.52412C10.2316 9.3955 9.78142 8.17364 9.55634 7.65917C9.36342 7.1447 9.13834 7.24116 9.00972 7.24116C8.8811 7.24116 8.68817 7.24116 8.55956 7.24116C8.43094 7.24116 8.1094 7.27331 7.91647 7.5627C7.69139 7.81994 7.0483 8.43087 7.0483 9.65273C7.0483 10.8746 7.91647 12 8.07724 12.2251C8.20586 12.3537 9.84573 14.8939 12.2895 15.9871C12.8682 16.2444 13.3184 16.4051 13.7043 16.5338C14.283 16.7267 14.8297 16.6624 15.2477 16.6302C15.73 16.5981 16.6946 16.0514 16.9197 15.4405C17.1126 14.8939 17.1126 14.3473 17.0483 14.2508C16.984 14.1865 16.7911 14.09 16.566 13.9936Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
)
}

View File

@@ -0,0 +1,41 @@
'use client'
import {useLocale} from 'next-intl'
import {redirect} from 'next/navigation'
import {Link, usePathname, useRouter} from '@/i18n/routing'
import {Label} from '@/ui/label'
import {Switch} from '@/ui/switch'
export default function LocaleSwitcher() {
const router = useRouter()
const pathname = usePathname()
const locale = useLocale()
const initialState = locale !== 'uk'
const handler = (state: boolean) => {
const newPath = `/${locale}${pathname}`
//window.history.replaceState(null, '', newPath)
const link = document.getElementById('lang-switch')
if (link) {
link.innerText = `${state ? '/ru' : ''}${pathname}`
link.setAttribute('href', `${state ? '/ru' : ''}${pathname}`)
link.click()
}
}
// router.replace('/cabinet', {locale: checked ? 'ru' : 'uk'}
return (
<div className='flex items-center space-x-2'>
<Link id='lang-switch' href='/' locale='uk'>
LA
</Link>
<Label htmlFor='locale-switcher'>Укр</Label>
<Switch
id='locale-switcher'
defaultChecked={initialState}
onCheckedChange={handler}
/>
<Label htmlFor='locale-switcher'>Рус</Label>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import NavbarMenu from '@/components/shared/navbar/navbar-menu'
export default function Navbar() {
return (
<nav className='text-min flex w-full items-center justify-between text-sm font-medium leading-none'>
<NavbarMenu />
</nav>
)
}

View File

@@ -0,0 +1,44 @@
'use client'
import {Menu as MenuIcon, X} from 'lucide-react'
import Link from 'next/link'
import {useState} from 'react'
import {data} from '@/lib/data'
import {Button} from '@/ui/button'
export default function NavbarMenu() {
const bp = 'md'
const [menuOpened, setMenuOpened] = useState(false)
function ToggleNavbar() {
setMenuOpened(!menuOpened)
}
return (
<>
<div className={`hidden ${bp}:block w-full`}>
<div className='flex items-center justify-between'>
{data.headerMenus.map(item => (
<Link href={item.href + item.slug} className='' key={item.name}>
{item.name}
</Link>
))}
</div>
</div>
<div className={`flex items-center ${bp}:hidden`}>
<Button variant='ghost' onClick={ToggleNavbar}>
{menuOpened ? <X /> : <MenuIcon />}
</Button>
</div>
{menuOpened && (
<div className={`${bp}:hidden`}>
<div className='space-y-1 px-2 pb-3 pt-2'>
<Link href={'#'}>Hidden Menu</Link>
</div>
</div>
)}
</>
)
}

View File

@@ -0,0 +1,33 @@
import {SearchIcon} from 'lucide-react'
import {getTranslations} from 'next-intl/server'
import Form from 'next/form'
import {Button} from '@/ui/button'
import {Input} from '@/ui/input'
export default async function SearchForm({query}: {query?: string}) {
const t = await getTranslations('UI')
return (
<Form
action='/search'
scroll={false}
className='border-stone flex h-10 w-full overflow-hidden rounded-[10px] border-2'
>
<Input
className='h-full flex-1 rounded-none border-0 bg-white text-base text-stone-600 outline-0 dark:border-gray-200'
placeholder={t('search-placeholder')}
name='query'
defaultValue={query}
type='text'
/>
<Button
type='submit'
className='h-full rounded-none border-none bg-neutral-200 px-3 py-2 text-stone-600'
>
{/*<SearchIcon className='size-5' />*/}
{t('search-button')}
</Button>
</Form>
)
}

View File

@@ -0,0 +1,55 @@
'use client'
import {useLocale} from 'next-intl'
import {Link} from '@/i18n/routing'
import {Button} from '@/ui/button'
export default function AppCatalogRender(items: Array<object>) {
const locale = useLocale()
return (
<div className='flex w-full justify-center'>
<div className='bw-dd-menu group inline-block w-full'>
<Button className='py-13 flex h-10 w-full items-center rounded-sm border-none bg-brand-yellow-300 px-3 outline-none focus:outline-none'>
<span className='flex-1 pr-1 font-semibold'>Каталог</span>
<span>
<svg
className='h-4 w-4 transform fill-current transition duration-300 ease-in-out group-hover:-rotate-180'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
>
<path d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z' />
</svg>
</span>
</Button>
<ul className='-bw-app-catalog-collapse mt-2 w-full min-w-32 origin-top transform rounded-sm border bg-white shadow-xl transition duration-300 ease-in-out group-hover:scale-100 hover:shadow-2xl'>
{items?.items.map((item: any) => (
<li
className='cursor-pointer rounded-none py-2.5 pl-3 pr-1.5 text-sm font-medium hover:bg-[#442d88]/10 xl:py-3'
key={item.id}
>
<button className='flex w-full items-center text-left outline-none focus:outline-none'>
<Link
className='flex-1 pr-1 leading-none xl:leading-[1.3]'
href={`/category/${item.id}-${item.locales[locale === 'uk' ? 0 : 1].slug}`}
>
{item.locales[locale === 'uk' ? 0 : 1].title}
</Link>
<span className='mr-auto'>
<svg
className='h-4 w-4 fill-current transition duration-150 ease-in-out'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 20 20'
>
<path d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z' />
</svg>
</span>
</button>
</li>
))}
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,16 @@
'use server'
import AppCatalogRender from '@/components/shared/sidebar/app-catalog-render'
import {db} from '@/lib/db/prisma/client'
const appCatalog = async () => {
return db.category.findMany({
include: {
locales: true
}
})
}
export default async function AppCatalog() {
return <AppCatalogRender items={await appCatalog()} />
}

View File

@@ -0,0 +1,82 @@
import IconProps from '@/components/shared/icons/props'
import WhatsappIcon from '@/components/shared/icons/whatsapp'
import {BRAND_ICON_COLOR, BRAND_ICON_SIZE} from '@/lib/constants'
import {cn} from '@/lib/utils'
export default function SocialMediaPanel({className, size, color}: IconProps) {
return (
<aside className={cn('flex items-center gap-x-4', className)}>
{/*facebook*/}
<div>
<svg
width={size || BRAND_ICON_SIZE}
height={size || BRAND_ICON_SIZE * 1.041666666666667}
viewBox='0 0 24 25'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M12 2.53906C17.5229 2.53906 22 7.01621 22 12.5391C22 17.5304 18.3431 21.6674 13.5625 22.4176V15.4297H15.8926L16.3359 12.5391L13.5625 12.5387V10.6632C13.5625 10.657 13.5625 10.6509 13.5626 10.6447C13.5626 10.6354 13.5628 10.6262 13.5629 10.6169C13.578 9.84259 13.9742 9.10156 15.1921 9.10156H16.4531V6.64062C16.4531 6.64062 15.3087 6.44492 14.2146 6.44492C11.966 6.44492 10.4842 7.78652 10.4386 10.2193C10.4379 10.2578 10.4375 10.2965 10.4375 10.3355V12.5387H7.89844V15.4293L10.4375 15.4297V22.4172C5.65686 21.667 2 17.5304 2 12.5391C2 7.01621 6.47715 2.53906 12 2.53906Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
<div>
<WhatsappIcon size={size} color={color} />
</div>
{/*Telegram*/}
<div>
<svg
width={size || BRAND_ICON_SIZE}
height={size || BRAND_ICON_SIZE}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M21.936 5.17077L18.9059 19.3546C18.6802 20.3539 18.1 20.5795 17.2618 20.1282L12.7166 16.7757L10.4923 18.9033C10.2666 19.1289 10.041 19.3546 9.5252 19.3546L9.8798 14.6804L18.3578 6.97598C18.7124 6.62138 18.2611 6.49244 17.8098 6.78256L7.26869 13.4232L2.72343 12.037C1.72412 11.7147 1.72412 11.0377 2.94908 10.5864L20.6144 3.72015C21.4847 3.46227 22.2262 3.91357 21.936 5.17077Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
{/*Instagram*/}
<div>
<svg
width={size || BRAND_ICON_SIZE}
height={size || BRAND_ICON_SIZE}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M8.6672 12C8.6672 10.1591 10.1591 8.6664 12 8.6664C13.8409 8.6664 15.3336 10.1591 15.3336 12C15.3336 13.8409 13.8409 15.3336 12 15.3336C10.1591 15.3336 8.6672 13.8409 8.6672 12ZM6.86512 12C6.86512 14.836 9.164 17.1349 12 17.1349C14.836 17.1349 17.1349 14.836 17.1349 12C17.1349 9.164 14.836 6.86512 12 6.86512C9.164 6.86512 6.86512 9.164 6.86512 12ZM16.1382 6.66152C16.1381 6.89886 16.2084 7.13089 16.3401 7.32829C16.4719 7.52568 16.6593 7.67956 16.8785 7.77047C17.0977 7.86138 17.339 7.88525 17.5718 7.83904C17.8046 7.79283 18.0185 7.67862 18.1863 7.51087C18.3542 7.34311 18.4686 7.12934 18.515 6.89658C18.5614 6.66382 18.5377 6.42253 18.447 6.20322C18.3563 5.98392 18.2025 5.79644 18.0052 5.6645C17.808 5.53257 17.576 5.4621 17.3386 5.462H17.3382C17.02 5.46215 16.715 5.58856 16.49 5.81347C16.265 6.03837 16.1384 6.34339 16.1382 6.66152ZM7.96 20.1398C6.98504 20.0954 6.45512 19.933 6.10296 19.7958C5.63608 19.614 5.30296 19.3975 4.95272 19.0478C4.60248 18.698 4.38568 18.3652 4.20472 17.8983C4.06744 17.5463 3.90504 17.0162 3.86072 16.0413C3.81224 14.9872 3.80256 14.6706 3.80256 12.0001C3.80256 9.3296 3.81304 9.01384 3.86072 7.95888C3.90512 6.98392 4.06872 6.45488 4.20472 6.10184C4.38648 5.63496 4.60296 5.30184 4.95272 4.9516C5.30248 4.60136 5.63528 4.38456 6.10296 4.2036C6.45496 4.06632 6.98504 3.90392 7.96 3.8596C9.01408 3.81112 9.33072 3.80144 12 3.80144C14.6693 3.80144 14.9862 3.81192 16.0412 3.8596C17.0162 3.904 17.5452 4.0676 17.8982 4.2036C18.3651 4.38456 18.6982 4.60184 19.0485 4.9516C19.3987 5.30136 19.6147 5.63496 19.7965 6.10184C19.9338 6.45384 20.0962 6.98392 20.1405 7.95888C20.189 9.01384 20.1986 9.3296 20.1986 12.0001C20.1986 14.6706 20.189 14.9863 20.1405 16.0413C20.0961 17.0162 19.9329 17.5462 19.7965 17.8983C19.6147 18.3652 19.3982 18.6983 19.0485 19.0478C18.6987 19.3972 18.3651 19.614 17.8982 19.7958C17.5462 19.933 17.0162 20.0954 16.0412 20.1398C14.9871 20.1882 14.6705 20.1979 12 20.1979C9.32952 20.1979 9.01376 20.1882 7.96 20.1398ZM7.8772 2.06056C6.81264 2.10904 6.0852 2.27784 5.44992 2.52504C4.792 2.78032 4.23504 3.1228 3.67848 3.67848C3.12192 4.23416 2.78032 4.792 2.52504 5.44992C2.27784 6.0856 2.10904 6.81264 2.06056 7.8772C2.01128 8.94344 2 9.28432 2 12C2 14.7157 2.01128 15.0566 2.06056 16.1228C2.10904 17.1874 2.27784 17.9144 2.52504 18.5501C2.78032 19.2076 3.122 19.7661 3.67848 20.3215C4.23496 20.877 4.792 21.219 5.44992 21.475C6.0864 21.7222 6.81264 21.891 7.8772 21.9394C8.944 21.9879 9.28432 22 12 22C14.7157 22 15.0566 21.9887 16.1228 21.9394C17.1874 21.891 17.9144 21.7222 18.5501 21.475C19.2076 21.219 19.765 20.8772 20.3215 20.3215C20.8781 19.7658 21.219 19.2076 21.475 18.5501C21.7222 17.9144 21.8918 17.1874 21.9394 16.1228C21.9879 15.0558 21.9992 14.7157 21.9992 12C21.9992 9.28432 21.9879 8.94344 21.9394 7.8772C21.891 6.81256 21.7222 6.0852 21.475 5.44992C21.219 4.7924 20.8772 4.23504 20.3215 3.67848C19.7658 3.12192 19.2076 2.78032 18.5509 2.52504C17.9144 2.27784 17.1874 2.10824 16.1236 2.06056C15.0574 2.01208 14.7165 2 12.0008 2C9.28512 2 8.944 2.01128 7.8772 2.06056Z'
fill={color || BRAND_ICON_COLOR}
/>
<path
d='M8.6672 12C8.6672 10.1591 10.1591 8.6664 12 8.6664C13.8409 8.6664 15.3336 10.1591 15.3336 12C15.3336 13.8409 13.8409 15.3336 12 15.3336C10.1591 15.3336 8.6672 13.8409 8.6672 12ZM6.86512 12C6.86512 14.836 9.164 17.1349 12 17.1349C14.836 17.1349 17.1349 14.836 17.1349 12C17.1349 9.164 14.836 6.86512 12 6.86512C9.164 6.86512 6.86512 9.164 6.86512 12ZM16.1382 6.66152C16.1381 6.89886 16.2084 7.13089 16.3401 7.32829C16.4719 7.52568 16.6593 7.67956 16.8785 7.77047C17.0977 7.86138 17.339 7.88525 17.5718 7.83904C17.8046 7.79283 18.0185 7.67862 18.1863 7.51087C18.3542 7.34311 18.4686 7.12934 18.515 6.89658C18.5614 6.66382 18.5377 6.42253 18.447 6.20322C18.3563 5.98392 18.2025 5.79644 18.0052 5.6645C17.808 5.53257 17.576 5.4621 17.3386 5.462H17.3382C17.02 5.46215 16.715 5.58856 16.49 5.81347C16.265 6.03837 16.1384 6.34339 16.1382 6.66152ZM7.96 20.1398C6.98504 20.0954 6.45512 19.933 6.10296 19.7958C5.63608 19.614 5.30296 19.3975 4.95272 19.0478C4.60248 18.698 4.38568 18.3652 4.20472 17.8983C4.06744 17.5463 3.90504 17.0162 3.86072 16.0413C3.81224 14.9872 3.80256 14.6706 3.80256 12.0001C3.80256 9.3296 3.81304 9.01384 3.86072 7.95888C3.90512 6.98392 4.06872 6.45488 4.20472 6.10184C4.38648 5.63496 4.60296 5.30184 4.95272 4.9516C5.30248 4.60136 5.63528 4.38456 6.10296 4.2036C6.45496 4.06632 6.98504 3.90392 7.96 3.8596C9.01408 3.81112 9.33072 3.80144 12 3.80144C14.6693 3.80144 14.9862 3.81192 16.0412 3.8596C17.0162 3.904 17.5452 4.0676 17.8982 4.2036C18.3651 4.38456 18.6982 4.60184 19.0485 4.9516C19.3987 5.30136 19.6147 5.63496 19.7965 6.10184C19.9338 6.45384 20.0962 6.98392 20.1405 7.95888C20.189 9.01384 20.1986 9.3296 20.1986 12.0001C20.1986 14.6706 20.189 14.9863 20.1405 16.0413C20.0961 17.0162 19.9329 17.5462 19.7965 17.8983C19.6147 18.3652 19.3982 18.6983 19.0485 19.0478C18.6987 19.3972 18.3651 19.614 17.8982 19.7958C17.5462 19.933 17.0162 20.0954 16.0412 20.1398C14.9871 20.1882 14.6705 20.1979 12 20.1979C9.32952 20.1979 9.01376 20.1882 7.96 20.1398ZM7.8772 2.06056C6.81264 2.10904 6.0852 2.27784 5.44992 2.52504C4.792 2.78032 4.23504 3.1228 3.67848 3.67848C3.12192 4.23416 2.78032 4.792 2.52504 5.44992C2.27784 6.0856 2.10904 6.81264 2.06056 7.8772C2.01128 8.94344 2 9.28432 2 12C2 14.7157 2.01128 15.0566 2.06056 16.1228C2.10904 17.1874 2.27784 17.9144 2.52504 18.5501C2.78032 19.2076 3.122 19.7661 3.67848 20.3215C4.23496 20.877 4.792 21.219 5.44992 21.475C6.0864 21.7222 6.81264 21.891 7.8772 21.9394C8.944 21.9879 9.28432 22 12 22C14.7157 22 15.0566 21.9887 16.1228 21.9394C17.1874 21.891 17.9144 21.7222 18.5501 21.475C19.2076 21.219 19.765 20.8772 20.3215 20.3215C20.8781 19.7658 21.219 19.2076 21.475 18.5501C21.7222 17.9144 21.8918 17.1874 21.9394 16.1228C21.9879 15.0558 21.9992 14.7157 21.9992 12C21.9992 9.28432 21.9879 8.94344 21.9394 7.8772C21.891 6.81256 21.7222 6.0852 21.475 5.44992C21.219 4.7924 20.8772 4.23504 20.3215 3.67848C19.7658 3.12192 19.2076 2.78032 18.5509 2.52504C17.9144 2.27784 17.1874 2.10824 16.1236 2.06056C15.0574 2.01208 14.7165 2 12.0008 2C9.28512 2 8.944 2.01128 7.8772 2.06056Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
{/*Youtube*/}
<div>
<svg
width={((size || BRAND_ICON_SIZE) as number) * 1.5}
height={((size || BRAND_ICON_SIZE) as number) * 1.5}
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M21.5806 7.19355C21.3548 6.32258 20.6774 5.64516 19.8065 5.41935C18.2581 5 12 5 12 5C12 5 5.74194 5 4.19355 5.41935C3.32258 5.64516 2.64516 6.32258 2.41935 7.19355C2 8.77419 2 12 2 12C2 12 2 15.2581 2.41935 16.8065C2.64516 17.6774 3.32258 18.3548 4.19355 18.5806C5.74194 19 12 19 12 19C12 19 18.2581 19 19.8065 18.5806C20.6774 18.3548 21.3548 17.6774 21.5806 16.8065C22 15.2581 22 12 22 12C22 12 22 8.77419 21.5806 7.19355ZM10 15V9L15.1935 12L10 15Z'
fill={color || BRAND_ICON_COLOR}
/>
</svg>
</div>
</aside>
)
}

View File

@@ -0,0 +1,30 @@
import Image from 'next/image'
import Link from 'next/link'
import {Card, CardContent, CardFooter} from '@/ui/card'
export type CardItem = {
title: string
image: string
href: string
price: number
}
export default function ProductCard({card}: {card: CardItem}) {
return (
<Card key={card.title} className='flex flex-col'>
<Link key={card.title} href={card.href} className='flex flex-col'>
<CardContent className='flex-1 p-4'>
<Image
src={card.image}
alt={card.title}
width={120}
height={120}
className='aspect-card mx-auto h-auto max-w-full object-cover'
/>
</CardContent>
<CardFooter>{card.title}</CardFooter>
</Link>
</Card>
)
}

View File

@@ -0,0 +1,31 @@
'use client'
import {useLocale} from 'next-intl'
import {Link} from '@/i18n/routing'
export default function TempComponent(data: object[]) {
const locale = useLocale()
/*console.log(data)*/
const items = data?.data as object[]
return (
<>
<code>{locale}</code>
{/*<pre>{JSON.stringify(data, null, 2)}</pre>*/}
<pre>
{items.map((item: any) => (
<div key={item.id}>
<Link
href={`/category/${item.id}-${item.locales[locale === 'uk' ? 0 : 1].slug}`}
>
{item.locales[locale === 'uk' ? 0 : 1].title}
</Link>
</div>
))}
</pre>
</>
)
}

50
components/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

59
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,59 @@
import {Slot} from '@radix-ui/react-slot'
import {type VariantProps, cva} from 'class-variance-authority'
import * as React from 'react'
import {cn} from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
brand:
'bg-brand-yellow text-primary-foreground shadow hover:bg-primary/90',
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({className, variant, size, asChild = false, ...props}, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({variant, size, className}))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
export {Button, buttonVariants}

76
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

266
components/ui/carousel.tsx Normal file
View File

@@ -0,0 +1,266 @@
'use client'
import useEmblaCarousel, {type UseEmblaCarouselType} from 'embla-carousel-react'
import {ArrowLeft, ArrowRight, ChevronLeft, ChevronRight} from 'lucide-react'
import * as React from 'react'
import {Button} from '@/components/ui/button'
import {cn} from '@/lib/utils'
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: 'horizontal' | 'vertical'
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error('useCarousel must be used within a <Carousel />')
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = 'horizontal',
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === 'horizontal' ? 'x' : 'y'
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault()
scrollPrev()
} else if (event.key === 'ArrowRight') {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on('reInit', onSelect)
api.on('select', onSelect)
return () => {
api?.off('select', onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn('relative', className)}
role='region'
aria-roledescription='carousel'
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = 'Carousel'
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => {
const {carouselRef, orientation} = useCarousel()
return (
<div ref={carouselRef} className='overflow-hidden'>
<div
ref={ref}
className={cn(
'flex',
orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = 'CarouselContent'
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({className, ...props}, ref) => {
const {orientation} = useCarousel()
return (
<div
ref={ref}
role='group'
aria-roledescription='slide'
className={cn(
'min-w-0 shrink-0 grow-0 basis-full',
orientation === 'horizontal' ? 'pl-4' : 'pt-4',
className
)}
{...props}
/>
)
})
CarouselItem.displayName = 'CarouselItem'
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({className, variant = 'outline', size = 'icon', ...props}, ref) => {
const {orientation, scrollPrev, canScrollPrev} = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
{/*<ArrowLeft className='h-4 w-4' />*/}
<ChevronLeft
size={72}
className='h-[78px] w-[78px]'
absoluteStrokeWidth
/>
<span className='sr-only'>Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = 'CarouselPrevious'
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({className, variant = 'outline', size = 'icon', ...props}, ref) => {
const {orientation, scrollNext, canScrollNext} = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
'absolute h-8 w-8 rounded-full',
orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
{/*<ArrowRight className='h-4 w-4' />*/}
<ChevronRight size={48} absoluteStrokeWidth />
<span className='sr-only'>Next slide</span>
</Button>
)
})
CarouselNext.displayName = 'CarouselNext'
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext
}

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

122
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,201 @@
'use client'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import {Check, ChevronRight, Circle} from 'lucide-react'
import * as React from 'react'
import {cn} from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({className, inset, children, ...props}, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open1]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className='ml-auto' />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({className, ...props}, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({className, sideOffset = 4, ...props}, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({className, inset, ...props}, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({className, children, checked, ...props}, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<Check className='h-4 w-4' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({className, children, ...props}, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
<DropdownMenuPrimitive.ItemIndicator>
<Circle className='h-2 w-2 fill-current' />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({className, inset, ...props}, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({className, ...props}, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup
}

178
components/ui/form.tsx Normal file
View File

@@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

23
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,23 @@
import * as React from 'react'
import {cn} from '@/lib/utils'
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({className, type, ...props}, ref) => {
return (
<input
type={type}
className={cn(
/*focus-visible:ring-1 focus-visible:ring-ring*/
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export {Input}

26
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

Some files were not shown because too many files have changed in this diff Show More