Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

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

1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<title>calmhn</title>
</head>
<body>
<script>document.documentElement.setAttribute('data-theme',localStorage.getItem('calmhn-theme')||'purple')</script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
14 changes: 9 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { ArrowSquareOut, ChatCircle, ArrowUp, Clock } from '@phosphor-icons/react'
import ThemeChooser from './ThemeChooser'

interface AlgoliaStory {
objectID: string
Expand Down Expand Up @@ -110,12 +111,15 @@ function App() {

return (
<div className="min-h-screen bg-neutral-50 relative">
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-orange-200 to-transparent pointer-events-none"></div>
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-accent-200 to-transparent pointer-events-none"></div>
<div className="max-w-3xl mx-auto px-6 py-12 relative">
<header className="mb-8">
<h1 className="text-4xl font-extrabold text-slate-900 tracking-tight">
Calm HN
</h1>
<div className="flex items-center justify-between">
<h1 className="text-4xl font-extrabold text-slate-900 tracking-tight">
Calm HN
</h1>
<ThemeChooser />
</div>
<p className="text-slate-500 text-[10px] mt-2 uppercase tracking-wider">
Top stories from the last three months
</p>
Expand All @@ -132,7 +136,7 @@ function App() {
aria-label={story.title}
/>
<div className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 relative z-10 pointer-events-none">
<span className="bg-slate-200 text-slate-500 text-[10px] leading-none font-medium px-2 py-0.5 rounded-full flex-shrink-0 self-center mt-px group-hover:bg-orange-200 group-hover:text-slate-600 transition-colors group-hover:duration-[750ms] duration-300">
<span className="bg-slate-200 text-slate-500 text-[10px] leading-none font-medium px-2 py-0.5 rounded-full flex-shrink-0 self-center mt-px group-hover:bg-accent-200 group-hover:text-slate-600 transition-colors group-hover:duration-[750ms] duration-300">
{index + 1}
</span>
<h2 className="text-slate-900 text-lg leading-relaxed">
Expand Down
30 changes: 30 additions & 0 deletions src/ThemeChooser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useTheme, type Theme } from './ThemeContext'

const themes: { name: Theme; color: string }[] = [
{ name: 'purple', color: '#a855f7' },
{ name: 'red', color: '#ef4444' },
{ name: 'blue', color: '#3b82f6' },
{ name: 'green', color: '#22c55e' },
]

export default function ThemeChooser() {
const { theme, setTheme } = useTheme()

return (
<div className="flex items-center gap-1.5">
{themes.map((t) => (
<button
key={t.name}
onClick={() => setTheme(t.name)}
aria-label={`${t.name} theme`}
className={`size-3.5 rounded-full transition-all duration-200 cursor-pointer ${
theme === t.name
? 'ring-2 ring-offset-2 ring-slate-400 scale-110'
: 'opacity-50 hover:opacity-100 hover:scale-110'
}`}
style={{ backgroundColor: t.color }}
/>
))}
</div>
)
}
40 changes: 40 additions & 0 deletions src/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'

export type Theme = 'purple' | 'red' | 'blue' | 'green'

interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

const STORAGE_KEY = 'calmhn-theme'
const VALID_THEMES: Theme[] = ['purple', 'red', 'blue', 'green']

function getInitialTheme(): Theme {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved && VALID_THEMES.includes(saved as Theme)) return saved as Theme
return 'purple'
}

export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getInitialTheme)

useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem(STORAGE_KEY, theme)
}, [theme])

return (
<ThemeContext.Provider value={{ theme, setTheme: setThemeState }}>
{children}
</ThemeContext.Provider>
)
}

export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}
20 changes: 20 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
@import "tailwindcss";

@theme {
--color-accent-200: #e9d5ff;
}

[data-theme="purple"] {
--color-accent-200: #e9d5ff;
}

[data-theme="red"] {
--color-accent-200: #fecaca;
}

[data-theme="blue"] {
--color-accent-200: #bfdbfe;
}

[data-theme="green"] {
--color-accent-200: #bbf7d0;
}

@font-face {
font-family: InterVariable;
font-style: normal;
Expand Down
5 changes: 4 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { ThemeProvider } from './ThemeContext.tsx'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>,
)
Loading