diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx index 393af46..5c41430 100644 --- a/app/share/[id]/page.tsx +++ b/app/share/[id]/page.tsx @@ -11,9 +11,8 @@ import TopReferrers from '@/components/dashboard/TopReferrers' import Locations from '@/components/dashboard/Locations' import TechSpecs from '@/components/dashboard/TechSpecs' import PerformanceStats from '@/components/dashboard/PerformanceStats' -import Select from '@/components/ui/Select' +import { Select, DatePicker as DatePickerModal } from '@ciphera-net/ui' import { LightningBoltIcon } from '@radix-ui/react-icons' -import DatePickerModal from '@/components/ui/DatePicker' // Helper to get date ranges const getDateRange = (days: number) => { diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 1732a0d..bfe9c12 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -8,8 +8,7 @@ import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, get import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format' import { toast } from 'sonner' import { LoadingOverlay } from '@ciphera-net/ui' -import Select from '@/components/ui/Select' -import DatePicker from '@/components/ui/DatePicker' +import { Select, DatePicker } from '@ciphera-net/ui' import ContentStats from '@/components/dashboard/ContentStats' import TopReferrers from '@/components/dashboard/TopReferrers' import Locations from '@/components/dashboard/Locations' diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx index c4b2d7b..86246c1 100644 --- a/app/sites/[id]/settings/page.tsx +++ b/app/sites/[id]/settings/page.tsx @@ -7,7 +7,7 @@ import { toast } from 'sonner' import { LoadingOverlay } from '@ciphera-net/ui' import VerificationModal from '@/components/sites/VerificationModal' import { PasswordInput } from '@ciphera-net/ui' -import Select from '@/components/ui/Select' +import { Select } from '@ciphera-net/ui' import { APP_URL, API_URL } from '@/lib/api/client' import { generatePrivacySnippet } from '@/lib/utils/privacySnippet' import { motion, AnimatePresence } from 'framer-motion' diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index 7020802..05bf02b 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -16,7 +16,7 @@ import type { TooltipProps } from 'recharts' import { formatNumber, formatDuration } from '@/lib/utils/format' import { ArrowTopRightIcon, ArrowBottomRightIcon, DownloadIcon, BarChartIcon } from '@radix-ui/react-icons' import { Button } from '@ciphera-net/ui' -import { Checkbox } from '@/components/ui/Checkbox' +import { Checkbox } from '@ciphera-net/ui' const COLORS = { brand: '#FD5E0F', diff --git a/components/dashboard/PerformanceStats.tsx b/components/dashboard/PerformanceStats.tsx index c8e5fff..61eb397 100644 --- a/components/dashboard/PerformanceStats.tsx +++ b/components/dashboard/PerformanceStats.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react' import { motion } from 'framer-motion' import { ChevronDownIcon } from '@radix-ui/react-icons' import { PerformanceStats as Stats, PerformanceByPageStat, getPerformanceByPage } from '@/lib/api/stats' -import Select from '@/components/ui/Select' +import { Select } from '@ciphera-net/ui' interface Props { stats: Stats diff --git a/components/dashboard/StatsCard.tsx b/components/dashboard/StatsCard.tsx deleted file mode 100644 index 63cb8bc..0000000 --- a/components/dashboard/StatsCard.tsx +++ /dev/null @@ -1,19 +0,0 @@ -'use client' - -interface StatsCardProps { - title: string - value: string -} - -export default function StatsCard({ title, value }: StatsCardProps) { - return ( -
-
- {title} -
-
- {value} -
-
- ) -} diff --git a/components/ui/Checkbox.tsx b/components/ui/Checkbox.tsx deleted file mode 100644 index 2ee5fc3..0000000 --- a/components/ui/Checkbox.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { CheckIcon } from '@radix-ui/react-icons'; - -export interface CheckboxProps extends Omit, 'onChange'> { - checked?: boolean; - onCheckedChange?: (checked: boolean) => void; - label?: React.ReactNode; -} - -export const Checkbox = React.forwardRef( - ({ className = '', checked, onCheckedChange, label, disabled, ...props }, ref) => { - return ( - - ); - } -); - -Checkbox.displayName = 'Checkbox'; diff --git a/components/ui/DatePicker.tsx b/components/ui/DatePicker.tsx deleted file mode 100644 index ee551a7..0000000 --- a/components/ui/DatePicker.tsx +++ /dev/null @@ -1,183 +0,0 @@ -'use client' - -import React, { useState, useEffect } from 'react' -import { ChevronLeftIcon, ChevronRightIcon, Cross2Icon } from '@radix-ui/react-icons' - -interface DateRange { - start: string - end: string -} - -interface DatePickerProps { - isOpen: boolean - onClose: () => void - onApply: (range: DateRange) => void - initialRange: DateRange -} - -export default function DatePicker({ isOpen, onClose, onApply, initialRange }: DatePickerProps) { - const [startDate, setStartDate] = useState(new Date(initialRange.start)) - const [endDate, setEndDate] = useState(new Date(initialRange.end)) - const [currentMonth, setCurrentMonth] = useState(new Date(initialRange.end)) - const [selectingStart, setSelectingStart] = useState(true) - - useEffect(() => { - if (isOpen) { - setStartDate(new Date(initialRange.start)) - setEndDate(new Date(initialRange.end)) - setCurrentMonth(new Date(initialRange.end)) - } - }, [isOpen, initialRange]) - - if (!isOpen) return null - - const getDaysInMonth = (date: Date) => { - const year = date.getFullYear() - const month = date.getMonth() - const days = new Date(year, month + 1, 0).getDate() - const firstDay = new Date(year, month, 1).getDay() - return { days, firstDay } - } - - const { days, firstDay } = getDaysInMonth(currentMonth) - - const handleDateClick = (day: number) => { - const clickedDate = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day) - - if (selectingStart) { - setStartDate(clickedDate) - // If clicked date is after current end date, reset end date - if (clickedDate > endDate) { - setEndDate(clickedDate) - } - setSelectingStart(false) - } else { - if (clickedDate < startDate) { - setStartDate(clickedDate) - setSelectingStart(false) // Keep selecting start effectively if they clicked before start - } else { - setEndDate(clickedDate) - setSelectingStart(true) // Reset to start for next interaction or just done - } - } - } - - const handleMonthChange = (increment: number) => { - const newMonth = new Date(currentMonth) - newMonth.setMonth(newMonth.getMonth() + increment) - setCurrentMonth(newMonth) - } - - const formatDate = (date: Date) => date.toISOString().split('T')[0] - - const isSelected = (day: number) => { - const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day) - return ( - date.getTime() === startDate.getTime() || - date.getTime() === endDate.getTime() - ) - } - - const isInRange = (day: number) => { - const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day) - return date > startDate && date < endDate - } - - const isToday = (day: number) => { - const today = new Date() - const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day) - return ( - date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() && - date.getFullYear() === today.getFullYear() - ) - } - - return ( -
-
-
-

Select Date Range

- -
- -
- - - {currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })} - - -
- -
- {['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => ( -
{day}
- ))} -
- -
- {Array.from({ length: firstDay }).map((_, i) => ( -
- ))} - {Array.from({ length: days }).map((_, i) => { - const day = i + 1 - const selected = isSelected(day) - const inRange = isInRange(day) - const today = isToday(day) - - return ( - - ) - })} -
- -
-
- - {startDate.toLocaleDateString()} - - {' - '} - - {endDate.toLocaleDateString()} - -
-
- - -
-
-
-
- ) -} diff --git a/components/ui/Select.tsx b/components/ui/Select.tsx deleted file mode 100644 index 18bb326..0000000 --- a/components/ui/Select.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client' - -import { useState, useRef, useEffect } from 'react' - -interface Option { - value: string - label: string -} - -interface SelectProps { - value: string - onChange: (value: string) => void - options: Option[] - className?: string - /** Form-field style (input-like). Default: toolbar style (btn-secondary). */ - variant?: 'default' | 'input' - /** Shown when value is empty or does not match any option. */ - placeholder?: string - /** Full-width trigger and panel. Use in form layouts. */ - fullWidth?: boolean - /** Id for the trigger (e.g. for label htmlFor). */ - id?: string - /** Alignment of the dropdown panel. */ - align?: 'left' | 'right' -} - -export default function Select({ - value, - onChange, - options, - className = '', - variant = 'default', - placeholder, - fullWidth = false, - id, - align = 'right', -}: SelectProps) { - const [isOpen, setIsOpen] = useState(false) - const ref = useRef(null) - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (ref.current && !ref.current.contains(event.target as Node)) { - setIsOpen(false) - } - } - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, []) - - const selectedOption = options.find((o) => o.value === value) - const displayLabel = selectedOption?.label ?? placeholder ?? value ?? '' - - const triggerBase = - variant === 'input' - ? 'px-4 py-2 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 text-neutral-900 dark:text-white text-left text-sm ' + - 'focus:outline-none focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 transition-all duration-200 ' + - (isOpen ? 'ring-4 ring-brand-orange/10 border-brand-orange' : '') - : 'btn-secondary min-w-[140px]' - - const triggerLayout = fullWidth ? 'w-full ' : '' - const alignClass = align === 'left' ? 'left-0' : 'right-0' - const panelMinW = fullWidth ? 'w-full' : 'min-w-[140px] w-full' - - return ( -
- - - {isOpen && ( -
- {options.map((option) => ( - - ))} -
- )} -
- ) -} diff --git a/package-lock.json b/package-lock.json index 0eeaedf..e8c7489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.1.0", "dependencies": { - "@ciphera-net/ui": "^0.0.13", + "@ciphera-net/ui": "^0.0.14", "@radix-ui/react-icons": "^1.3.2", "axios": "^1.13.2", "country-flag-icons": "^1.6.4", @@ -268,9 +268,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.13", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.13/0bcb7f064f6212cf701f647bc07596530e9db9c9", - "integrity": "sha512-edp2igwPpZDdwHol+jkqdRU3oDFxijyBQtLwjjuoZc2/hnGmHJRsnpKzd4Eo0tY1PuWrHv/QP4nDgYPG2819CQ==", + "version": "0.0.14", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.14/fb56b1bbd138eddc5a16d26c26d58524821f78c8", + "integrity": "sha512-IcQnp8pr7qsCU1QLKCUad7i+H0l/MykwHiu7pvbEON31PeFEJj8pdkXYnp+0ihRunWQ73G5Jik44AZtqHgNyFg==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index dc91ceb..2ac6828 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.13", + "@ciphera-net/ui": "^0.0.14", "@radix-ui/react-icons": "^1.3.2", "axios": "^1.13.2", "country-flag-icons": "^1.6.4", diff --git a/styles/globals.css b/styles/globals.css index 94264cb..4c3ece6 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -8,54 +8,13 @@ --color-success: #10B981; --color-warning: #F59E0B; --color-error: #EF4444; - - /* * Brand colors - Orange used as accent only */ - --color-brand-orange: #FD5E0F; - - /* * Neutral greys for UI */ - --color-neutral-50: #fafafa; - --color-neutral-100: #f5f5f5; - --color-neutral-200: #e5e5e5; - --color-neutral-300: #d4d4d4; - --color-neutral-400: #a3a3a3; - --color-neutral-500: #737373; - --color-neutral-600: #525252; - --color-neutral-700: #404040; - --color-neutral-800: #262626; - --color-neutral-900: #171717; - - /* * Dark mode support */ - --color-bg: #ffffff; - --color-text: #171717; - } - - .dark { - --color-bg: #0a0a0a; - --color-text: #fafafa; - } - - * { - @apply border-neutral-200 dark:border-neutral-800 transition-colors duration-300 ease-in-out; } body { - @apply bg-white dark:bg-neutral-950 text-neutral-900 dark:text-neutral-50 transition-colors duration-300 ease-in-out; @apply bg-ciphera-gradient bg-fixed; - font-family: var(--font-plus-jakarta-sans), system-ui, sans-serif; } .dark body { @apply bg-ciphera-gradient-dark; } } - -@layer components { - /* * Reusable component styles */ - .btn-primary { - @apply bg-brand-orange text-white px-5 py-2.5 rounded-xl font-semibold shadow-sm shadow-orange-200 dark:shadow-none hover:shadow-orange-300 dark:hover:shadow-brand-orange/20 hover:-translate-y-0.5 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2 dark:focus:ring-offset-neutral-900; - } - - .btn-secondary { - @apply bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white px-5 py-2.5 rounded-xl font-medium hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all duration-200 shadow-sm hover:shadow-md dark:shadow-none focus:outline-none focus:ring-2 focus:ring-neutral-200 dark:focus:ring-neutral-700 focus:ring-offset-2 dark:focus:ring-offset-neutral-900; - } -}