Skip to content

Commit

Permalink
Added profile button (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
dohsimpson authored Jan 4, 2025
1 parent f04a5e4 commit fadf33e
Show file tree
Hide file tree
Showing 12 changed files with 722 additions and 128 deletions.
55 changes: 55 additions & 0 deletions Budfile
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,58 @@ run() {
build() {
npm run build
}

add_changelog() {
# Get current version from package.json
current_version=$(node -p "require('./package.json').version")

# Ask if this is for a new version
echo "Is this for a new version? (y/n)"
read -r is_new_version

if [[ "$is_new_version" =~ ^[Yy]$ ]]; then
new_version=$(node -p "require('./package.json').version")

# Add new version header
sed -i "/^# Changelog/a \\\n## Version $new_version" CHANGELOG.md
fi

while true; do
# Get change type
echo "What type of change is this? ([A]dded/[C]hanged/[F]ixed/[R]emoved)"
read -r change_type

case $change_type in
A | a) change_type="Added" ;;
C | c) change_type="Changed" ;;
F | f) change_type="Fixed" ;;
R | r) change_type="Removed" ;;
*)
echo "Invalid change type"
continue
;;
esac

# Get change description
echo "Enter description of the change:"
read -r change_desc

# Add the change to CHANGELOG.md
if grep -q "^### $change_type" CHANGELOG.md; then
# If type exists, append to it
sed -i "/^### $change_type$/a - $change_desc" CHANGELOG.md
else
# If type doesn't exist, create new section
sed -i "/^## Version/ {N; s/\n/\n\n### $change_type\n- $change_desc\n/}" CHANGELOG.md
fi

echo "Change added successfully!"

# Ask if user wants to add another change
echo "Add another change? (y/n)"
read -r add_another
[[ "$add_another" =~ ^[Yy]$ ]] || break
done

echo "Changelog update complete!"
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## Version 0.1.11

### Added

- profile button
- settings to update profile image

### Changed

- Move settings and about to under profile button

## Version 0.1.10

### Fixed
Expand Down
35 changes: 35 additions & 0 deletions app/actions/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,41 @@ export async function removeCoins(
return newData
}

export async function uploadAvatar(formData: FormData) {
const file = formData.get('avatar') as File
if (!file) throw new Error('No file provided')

if (file.size > 5 * 1024 * 1024) { // 5MB
throw new Error('File size must be less than 5MB')
}

// Create avatars directory if it doesn't exist
const avatarsDir = path.join(process.cwd(), 'data', 'avatars')
await fs.mkdir(avatarsDir, { recursive: true })

// Generate unique filename
const ext = file.name.split('.').pop()
const filename = `${Date.now()}.${ext}`
const filePath = path.join(avatarsDir, filename)

// Save file
const buffer = await file.arrayBuffer()
await fs.writeFile(filePath, Buffer.from(buffer))

// Update settings with new avatar path
const settings = await loadSettings()
const newSettings = {
...settings,
profile: {
...settings.profile,
avatarPath: `/data/avatars/${filename}`
}
}

await saveSettings(newSettings)
return newSettings;
}

export async function getChangelog(): Promise<string> {
try {
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md')
Expand Down
26 changes: 26 additions & 0 deletions app/api/avatars/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from 'next/server'
import fs from 'fs/promises'
import path from 'path'

export async function GET(
request: Request,
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const { path: pathSegments } = await Promise.resolve(params)
const filePath = path.join(process.cwd(), 'data', 'avatars', ...(pathSegments || []))
const file = await fs.readFile(filePath)
const ext = path.extname(filePath).slice(1)

return new NextResponse(file, {
headers: {
'Content-Type': `image/${ext}`,
},
})
} catch (error) {
return NextResponse.json(
{ error: 'File not found' },
{ status: 404 }
)
}
}
209 changes: 134 additions & 75 deletions app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR'
import { useAtom } from 'jotai'
import { settingsAtom } from '@/lib/atoms'
import { Settings } from '@/lib/types'
import { saveSettings } from '../actions/data'
import { saveSettings, uploadAvatar } from '../actions/data'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { User } from 'lucide-react'

export default function SettingsPage() {
const [settings, setSettings] = useAtom(settingsAtom)
Expand All @@ -21,88 +24,144 @@ export default function SettingsPage() {
if (!settings) return null

return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Settings</h1>
<Card className="mb-6">
<CardHeader>
<CardTitle>UI Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="number-formatting">Number Formatting</Label>
<div className="text-sm text-muted-foreground">
Format large numbers (e.g., 1K, 1M, 1B)
</div>
</div>
<Switch
id="number-formatting"
checked={settings.ui.useNumberFormatting}
onCheckedChange={(checked) =>
updateSettings({
...settings,
ui: { ...settings.ui, useNumberFormatting: checked }
})
}
/>
</div>

<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="number-grouping">Number Grouping</Label>
<div className="text-sm text-muted-foreground">
Use thousand separators (e.g., 1,000 vs 1000)
<>
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Settings</h1>
<Card className="mb-6">
<CardHeader>
<CardTitle>UI Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="number-formatting">Number Formatting</Label>
<div className="text-sm text-muted-foreground">
Format large numbers (e.g., 1K, 1M, 1B)
</div>
</div>
<Switch
id="number-formatting"
checked={settings.ui.useNumberFormatting}
onCheckedChange={(checked) =>
updateSettings({
...settings,
ui: { ...settings.ui, useNumberFormatting: checked }
})
}
/>
</div>
<Switch
id="number-grouping"
checked={settings.ui.useGrouping}
onCheckedChange={(checked) =>
updateSettings({
...settings,
ui: { ...settings.ui, useGrouping: checked }
})
}
/>
</div>
</CardContent>
</Card>

<Card className="mb-6">
<CardHeader>
<CardTitle>System Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="timezone">Timezone</Label>
<div className="text-sm text-muted-foreground">
Select your timezone for accurate date tracking
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="number-grouping">Number Grouping</Label>
<div className="text-sm text-muted-foreground">
Use thousand separators (e.g., 1,000 vs 1000)
</div>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<select
id="timezone"
value={settings.system.timezone}
onChange={(e) =>
<Switch
id="number-grouping"
checked={settings.ui.useGrouping}
onCheckedChange={(checked) =>
updateSettings({
...settings,
system: { ...settings.system, timezone: e.target.value }
ui: { ...settings.ui, useGrouping: checked }
})
}
className="w-[200px] rounded-md border border-input bg-background px-3 py-2"
>
{Intl.supportedValuesOf('timeZone').map((tz) => (
<option key={tz} value={tz}>
{tz}
</option>
))}
</select>
<DynamicTimeNoSSR />
/>
</div>
</CardContent>
</Card>

<Card className="mb-6">
<CardHeader>
<CardTitle>System Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="timezone">Timezone</Label>
<div className="text-sm text-muted-foreground">
Select your timezone for accurate date tracking
</div>
</div>
<div className="flex flex-col items-end gap-2">
<select
id="timezone"
value={settings.system.timezone}
onChange={(e) =>
updateSettings({
...settings,
system: { ...settings.system, timezone: e.target.value }
})
}
className="w-[200px] rounded-md border border-input bg-background px-3 py-2"
>
{Intl.supportedValuesOf('timeZone').map((tz) => (
<option key={tz} value={tz}>
{tz}
</option>
))}
</select>
<DynamicTimeNoSSR />
</div>
</div>
</CardContent>
</Card>
<Card className="mb-6">
<CardHeader>
<CardTitle>Profile Settings</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="avatar">Avatar</Label>
<div className="text-sm text-muted-foreground">
Customize your profile picture
</div>
</div>
<div className="flex items-center gap-4">
<Avatar className="h-16 w-16">
<AvatarImage src={settings.profile?.avatarPath ? `/api/avatars/${settings.profile.avatarPath.split('/').pop()}` : '/avatars/default.png'} />
<AvatarFallback>
<User className="h-8 w-8" />
</AvatarFallback>
</Avatar>
<form action={async (formData: FormData) => {
const newSettings = await uploadAvatar(formData)
setSettings(newSettings)
}}>
<input
type="file"
id="avatar"
name="avatar"
accept="image/png, image/jpeg"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
if (file.size > 5 * 1024 * 1024) { // 5MB
alert('File size must be less than 5MB')
e.target.value = ''
return
}
const form = e.target.form
if (form) form.requestSubmit()
}
}}
/>
<Button
type="button"
variant="outline"
onClick={() => document.getElementById('avatar')?.click()}
>
Change
</Button>
</form>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
</div>
</>
)
}
Loading

0 comments on commit fadf33e

Please sign in to comment.