Skip to content

Commit

Permalink
feat: add theme-provider and providers setup
Browse files Browse the repository at this point in the history
  • Loading branch information
irsyadadl committed Aug 9, 2024
1 parent 7f42001 commit f1bb40d
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ dist/
.env
tailwind.config.js
tailwind.config.ts
app

src/app
src/index.css
47 changes: 42 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@inquirer/prompts": "^5.1.2",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"justd-icons": "^1.4.29",
"ora": "^8.0.1"
},
"release-it": {
Expand Down
46 changes: 39 additions & 7 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const __dirname = path.dirname(__filename)

// Adjust the path to reference the correct resource directory relative to the compiled output
const resourceDir = path.resolve(__dirname, '../src/resources')
const stubs = path.resolve(__dirname, '../src/resources/stubs')

export async function init() {
const cssPath = {
Expand Down Expand Up @@ -44,18 +45,21 @@ export async function init() {
{ name: 'Other', value: 'Other' },
],
})
let componentsFolder, uiFolder, cssLocation, configSourcePath
let componentsFolder, uiFolder, cssLocation, configSourcePath, themeProvider, providers

if (projectType === 'Laravel') {
componentsFolder = 'resources/js/components'
uiFolder = path.join(componentsFolder, 'ui')
cssLocation = cssPath.laravel
configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.laravel.stub')
configSourcePath = path.join(stubs, 'laravel/tailwind.config.laravel.stub')
themeProvider = path.join(stubs, 'laravel/theme-provider.stub')
providers = path.join(stubs, 'laravel/providers.stub')
} else if (projectType === 'Vite') {
componentsFolder = 'src/components'
uiFolder = path.join(componentsFolder, 'ui')
cssLocation = cssPath.vite
configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.vite.stub')
configSourcePath = path.join(stubs, 'vite/tailwind.config.vite.stub')
themeProvider = path.join(stubs, 'vite/theme-provider.stub')
} else if (projectType === 'Next.js') {
const projectTypeSrc = await select({
message: 'Does this project have a src directory?',
Expand All @@ -69,7 +73,9 @@ export async function init() {
componentsFolder = path.join(hasSrc, 'components')
uiFolder = path.join(componentsFolder, 'ui')
cssLocation = projectTypeSrc ? cssPath.nextHasSrc : cssPath.nextNoSrc
configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.next.stub')
configSourcePath = path.join(stubs, 'next/tailwind.config.next.stub')
themeProvider = path.join(stubs, 'next/theme-provider.stub')
providers = path.join(stubs, 'next/providers.stub')
} else {
componentsFolder = await input({
message: 'Enter the path to your components folder:',
Expand All @@ -80,10 +86,12 @@ export async function init() {
message: 'Where would you like to place the CSS file?',
default: cssPath.other,
})
configSourcePath = path.join(resourceDir, 'tailwind-config/tailwind.config.next.stub')
configSourcePath = path.join(stubs, 'next/tailwind.config.next.stub')
themeProvider = path.join(stubs, 'next/theme-provider.stub')
providers = path.join(stubs, 'next/providers.stub')
}

const spinner = ora(`Initializing D...`).start()
const spinner = ora(`Initializing Justd...`).start()

// Ensure the components and UI folders exist
if (!fs.existsSync(uiFolder)) {
Expand Down Expand Up @@ -159,9 +167,22 @@ export async function init() {
const fileUrl = 'https://raw.githubusercontent.com/irsyadadl/justd/master/components/ui/primitive.tsx'
const response = await fetch(fileUrl)
const fileContent = await response.text()
fs.writeFileSync(path.join(uiFolder, 'primitive.tsx'), fileContent)
fs.writeFileSync(path.join(uiFolder, 'primitive.tsx'), fileContent, { flag: 'w' })
spinner.succeed(`primitive.tsx file copied to ${uiFolder}`)

// Copy theme provider and providers files
if (themeProvider) {
const themeProviderContent = fs.readFileSync(themeProvider, 'utf8')
fs.writeFileSync(path.join(componentsFolder, 'theme-provider.tsx'), themeProviderContent, { flag: 'w' })

if (providers) {
const providersContent = fs.readFileSync(providers, 'utf8')
fs.writeFileSync(path.join(componentsFolder, 'providers.tsx'), providersContent, { flag: 'w' })
}

spinner.succeed(`Theme provider and providers files copied to ${componentsFolder}`)
}

// Save configuration to justd.json with relative path
if (fs.existsSync('d.json')) {
fs.unlinkSync('d.json')
Expand All @@ -177,5 +198,16 @@ export async function init() {

// Wait for the installation to complete before proceeding
spinner.succeed('Installation complete.')

const continuedToAddComponent = spawn('npx justd-cli@latest add', {
stdio: 'inherit',
shell: true,
})
await new Promise<void>((resolve) => {
continuedToAddComponent.on('close', () => {
resolve()
})
})

spinner.stop()
}
14 changes: 14 additions & 0 deletions src/resources/stubs/laravel/providers.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { router } from '@inertiajs/react'
import { ThemeProvider } from './theme-provider'
import React from 'react'
import { RouterProvider } from 'react-aria-components'

export function Providers({ children }: { children: React.ReactNode }) {
return (
<RouterProvider navigate={(to, options) => router.visit(to, options as any)}>
<ThemeProvider defaultTheme="system" storageKey="ui-theme">
{children}
</ThemeProvider>
</RouterProvider>
)
}
67 changes: 67 additions & 0 deletions src/resources/stubs/laravel/theme-provider.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createContext, useContext, useEffect, useState } from 'react'

type Theme = 'dark' | 'light' | 'system'

type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}

type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}

const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null
}

const ThemeProviderContext = createContext<ThemeProviderState>(initialState)

export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme)

useEffect(() => {
const root = window.document.documentElement

root.classList.remove('light', 'dark')

if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'

root.classList.add(systemTheme)
return
}

root.classList.add(theme)
}, [theme])

const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
}
}

return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}

export const useTheme = () => {
const context = useContext(ThemeProviderContext)

if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider')

return context
}
21 changes: 21 additions & 0 deletions src/resources/stubs/next/providers.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client'

import { ThemeProvider } from './theme-provider'
import { useRouter } from 'next/navigation'
import { RouterProvider } from 'react-aria-components'

declare module 'react-aria-components' {
interface RouterConfig {
routerOptions: NonNullable<Parameters<ReturnType<typeof useRouter>['push']>[1]>
}
}

export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter()

return (
<RouterProvider navigate={router.push}>
<ThemeProvider attribute="class">{children}</ThemeProvider>
</RouterProvider>
)
}
12 changes: 12 additions & 0 deletions src/resources/stubs/next/theme-provider.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use client'

import * as React from 'react'

import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'

const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

export { ThemeProvider, useTheme }
67 changes: 67 additions & 0 deletions src/resources/stubs/vite/theme-provider.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createContext, useContext, useEffect, useState } from 'react'

type Theme = 'dark' | 'light' | 'system'

type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}

type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}

const initialState: ThemeProviderState = {
theme: 'system',
setTheme: () => null
}

const ThemeProviderContext = createContext<ThemeProviderState>(initialState)

export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'vite-ui-theme',
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || defaultTheme)

useEffect(() => {
const root = window.document.documentElement

root.classList.remove('light', 'dark')

if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'

root.classList.add(systemTheme)
return
}

root.classList.add(theme)
}, [theme])

const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
}
}

return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}

export const useTheme = () => {
const context = useContext(ThemeProviderContext)

if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider')

return context
}

0 comments on commit f1bb40d

Please sign in to comment.