init: kovakemlak-crm project scaffold
- Next.js 16 + Appwrite multi-tenant emlak CRM - Database: kovakemlak-db (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings) - Same stack as isletmem-kovakcrm (shadcn/ui template base) - Modules: portföy, müşteri takibi, arama kriterleri, otomatik eşleştirme, sunum linki, yatırımcı portalı
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"use client"
|
||||
|
||||
import { useRef, useCallback } from "react"
|
||||
import { useTheme } from "@/hooks/use-theme"
|
||||
|
||||
interface CircularTransitionHook {
|
||||
startTransition: (coords: { x: number; y: number }, callback: () => void) => void
|
||||
toggleTheme: (event: React.MouseEvent) => void
|
||||
isTransitioning: () => boolean
|
||||
}
|
||||
|
||||
export function useCircularTransition(): CircularTransitionHook {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const isTransitioningRef = useRef(false)
|
||||
|
||||
const startTransition = useCallback((coords: { x: number; y: number }, callback: () => void) => {
|
||||
if (isTransitioningRef.current) return
|
||||
|
||||
isTransitioningRef.current = true
|
||||
|
||||
// Set CSS variables for the circular reveal animation - exactly like tweakcn
|
||||
const x = (coords.x / window.innerWidth) * 100
|
||||
const y = (coords.y / window.innerHeight) * 100
|
||||
|
||||
// Set the CSS variables on document element
|
||||
document.documentElement.style.setProperty('--x', `${x}%`)
|
||||
document.documentElement.style.setProperty('--y', `${y}%`)
|
||||
|
||||
// Check if View Transitions API is supported
|
||||
if ('startViewTransition' in document) {
|
||||
const transition = (document as Document & { startViewTransition: (callback: () => void) => { finished: Promise<void> } }).startViewTransition(() => {
|
||||
callback()
|
||||
})
|
||||
|
||||
transition.finished.finally(() => {
|
||||
isTransitioningRef.current = false
|
||||
})
|
||||
} else {
|
||||
// Fallback for browsers without View Transitions API
|
||||
callback()
|
||||
setTimeout(() => {
|
||||
isTransitioningRef.current = false
|
||||
}, 400)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleTheme = useCallback((event: React.MouseEvent) => {
|
||||
// Get precise click coordinates - use clientX/clientY directly like tweakcn
|
||||
const coords = {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
|
||||
startTransition(coords, () => {
|
||||
setTheme(theme === "dark" ? "light" : "dark")
|
||||
})
|
||||
}, [theme, setTheme, startTransition])
|
||||
|
||||
const isTransitioning = useCallback(() => {
|
||||
return isTransitioningRef.current
|
||||
}, [])
|
||||
|
||||
return {
|
||||
startTransition,
|
||||
toggleTheme,
|
||||
isTransitioning
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
export function useFullscreen() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setIsFullscreen(!!document.fullscreenElement)
|
||||
}
|
||||
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange)
|
||||
|
||||
// Initial check
|
||||
setIsFullscreen(!!document.fullscreenElement)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const enterFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (isFullscreen) {
|
||||
exitFullscreen()
|
||||
} else {
|
||||
enterFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isFullscreen,
|
||||
enterFullscreen,
|
||||
exitFullscreen,
|
||||
toggleFullscreen,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = typeof window !== "undefined" ? window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) : null
|
||||
const onChange = () => {
|
||||
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false)
|
||||
}
|
||||
|
||||
if (mql) {
|
||||
mql.addEventListener("change", onChange)
|
||||
}
|
||||
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false)
|
||||
|
||||
return () => {
|
||||
if (mql) {
|
||||
mql.removeEventListener("change", onChange)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from "react"
|
||||
import { SidebarContext, type SidebarContextValue } from "@/contexts/sidebar-context"
|
||||
|
||||
export function useSidebarConfig(): SidebarContextValue {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebarConfig must be used within a SidebarConfigProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
"use client"
|
||||
|
||||
import React from 'react'
|
||||
import { useTheme } from '@/hooks/use-theme'
|
||||
import { baseColors } from '@/config/theme-customizer-constants'
|
||||
import { colorThemes } from '@/config/theme-data'
|
||||
import type { ThemePreset, ImportedTheme } from '@/types/theme-customizer'
|
||||
|
||||
export function useThemeManager() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [brandColorsValues, setBrandColorsValues] = React.useState<Record<string, string>>({})
|
||||
|
||||
// Simple, reliable theme detection - just follow the theme provider
|
||||
const isDarkMode = React.useMemo(() => {
|
||||
if (theme === "dark") return true
|
||||
if (theme === "light") return false
|
||||
return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
}, [theme])
|
||||
|
||||
const resetTheme = React.useCallback(() => {
|
||||
// Comprehensive reset of ALL possible CSS variables that could be set by themes
|
||||
const root = document.documentElement
|
||||
const allPossibleVars = [
|
||||
// Standard shadcn/ui variables
|
||||
'background', 'foreground', 'card', 'card-foreground', 'popover', 'popover-foreground',
|
||||
'primary', 'primary-foreground', 'secondary', 'secondary-foreground', 'muted', 'muted-foreground',
|
||||
'accent', 'accent-foreground', 'destructive', 'destructive-foreground', 'border', 'input',
|
||||
'ring', 'radius',
|
||||
|
||||
// Chart variables
|
||||
'chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5',
|
||||
|
||||
// Sidebar variables
|
||||
'sidebar', 'sidebar-background', 'sidebar-foreground', 'sidebar-primary', 'sidebar-primary-foreground',
|
||||
'sidebar-accent', 'sidebar-accent-foreground', 'sidebar-border', 'sidebar-ring',
|
||||
|
||||
// Font variables that might be in imported themes
|
||||
'font-sans', 'font-serif', 'font-mono',
|
||||
|
||||
// Shadow variables from imported themes
|
||||
'shadow-2xs', 'shadow-xs', 'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'shadow-2xl',
|
||||
|
||||
// Spacing variables
|
||||
'spacing', 'tracking-normal',
|
||||
|
||||
// Additional variables that might be set by advanced themes
|
||||
'card-header', 'card-content', 'card-footer', 'muted-background', 'accent-background',
|
||||
'destructive-background', 'warning', 'warning-foreground', 'success', 'success-foreground',
|
||||
'info', 'info-foreground'
|
||||
]
|
||||
|
||||
// Remove all possible CSS variables
|
||||
allPossibleVars.forEach(varName => {
|
||||
root.style.removeProperty(`--${varName}`)
|
||||
})
|
||||
|
||||
// Also remove any inline styles that might have been set (comprehensive cleanup)
|
||||
const inlineStyles = root.style
|
||||
for (let i = inlineStyles.length - 1; i >= 0; i--) {
|
||||
const property = inlineStyles[i]
|
||||
if (property.startsWith('--')) {
|
||||
root.style.removeProperty(property)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const updateBrandColorsFromTheme = React.useCallback((styles: Record<string, string>) => {
|
||||
const newValues: Record<string, string> = {}
|
||||
baseColors.forEach(color => {
|
||||
const cssVar = color.cssVar.replace('--', '')
|
||||
if (styles[cssVar]) {
|
||||
newValues[color.cssVar] = styles[cssVar]
|
||||
}
|
||||
})
|
||||
setBrandColorsValues(newValues)
|
||||
}, [])
|
||||
|
||||
const applyTheme = React.useCallback((themeValue: string, darkMode: boolean) => {
|
||||
const theme = colorThemes.find(t => t.value === themeValue)
|
||||
if (!theme) return
|
||||
|
||||
// Reset and apply theme variables
|
||||
resetTheme()
|
||||
const styles = darkMode ? theme.preset.styles.dark : theme.preset.styles.light
|
||||
const root = document.documentElement
|
||||
|
||||
Object.entries(styles).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--${key}`, value)
|
||||
})
|
||||
|
||||
// Update brand colors values when theme changes
|
||||
updateBrandColorsFromTheme(styles)
|
||||
}, [resetTheme, updateBrandColorsFromTheme])
|
||||
|
||||
const applyTweakcnTheme = React.useCallback((themePreset: ThemePreset, darkMode: boolean) => {
|
||||
// Reset and apply theme variables
|
||||
resetTheme()
|
||||
const styles = darkMode ? themePreset.styles.dark : themePreset.styles.light
|
||||
const root = document.documentElement
|
||||
|
||||
Object.entries(styles).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--${key}`, value)
|
||||
})
|
||||
|
||||
// Update brand colors values when theme changes
|
||||
updateBrandColorsFromTheme(styles)
|
||||
}, [resetTheme, updateBrandColorsFromTheme])
|
||||
|
||||
const applyImportedTheme = React.useCallback((themeData: ImportedTheme, darkMode: boolean) => {
|
||||
const root = document.documentElement
|
||||
const themeVars = darkMode ? themeData.dark : themeData.light
|
||||
|
||||
// Apply all variables from the theme
|
||||
Object.entries(themeVars).forEach(([variable, value]) => {
|
||||
root.style.setProperty(`--${variable}`, value)
|
||||
})
|
||||
|
||||
// Update brand colors values for the customizer UI
|
||||
const newBrandColors: Record<string, string> = {}
|
||||
baseColors.forEach(color => {
|
||||
const varName = color.cssVar.replace('--', '')
|
||||
if (themeVars[varName]) {
|
||||
newBrandColors[color.cssVar] = themeVars[varName]
|
||||
}
|
||||
})
|
||||
setBrandColorsValues(newBrandColors)
|
||||
}, [])
|
||||
|
||||
const applyRadius = (radius: string) => {
|
||||
document.documentElement.style.setProperty('--radius', radius)
|
||||
}
|
||||
|
||||
const handleColorChange = (cssVar: string, value: string) => {
|
||||
document.documentElement.style.setProperty(cssVar, value)
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
setTheme,
|
||||
isDarkMode,
|
||||
brandColorsValues,
|
||||
setBrandColorsValues,
|
||||
resetTheme,
|
||||
applyTheme,
|
||||
applyTweakcnTheme,
|
||||
applyImportedTheme,
|
||||
applyRadius,
|
||||
handleColorChange,
|
||||
updateBrandColorsFromTheme
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as React from "react"
|
||||
import { ThemeProviderContext } from "@/contexts/theme-context"
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = React.useContext(ThemeProviderContext)
|
||||
|
||||
if (context === undefined)
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user