Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8b9c650
Add GNU GPL v3 license
andreasmolnardev Oct 22, 2025
777a9f4
display username from local storage pb_user
m4sc0 Oct 22, 2025
46bf48e
added name as field to SignupForm
m4sc0 Oct 22, 2025
d1dfd02
forgot value on input
m4sc0 Oct 22, 2025
65192b8
added name in api route for signup
m4sc0 Oct 22, 2025
864253b
Update README with new features and enhancements
andreasmolnardev Oct 22, 2025
f53fb0e
Merge branch 'dev' of github.com:andreasmolnardev/dashwise-next into …
m4sc0 Oct 22, 2025
e9bb91a
added fallback to non given name
m4sc0 Oct 23, 2025
07d2cc4
Merge pull request #39 from m4sc0/9-ask-user-for-name-on-signup
andreasmolnardev Oct 23, 2025
a271ab2
bug/fixed world clock to use custom Select component
m4sc0 Oct 23, 2025
ea5dc4f
bug/fixed enum display as well
m4sc0 Oct 23, 2025
a2de2e8
Merge pull request #42 from m4sc0/36-bug-change-the-timezone
andreasmolnardev Oct 23, 2025
e74b46e
fix/credentials are stripped from config
m4sc0 Oct 23, 2025
162404c
accidental import of an unused var
m4sc0 Oct 23, 2025
3ffb3ff
update default conig to include search engines
andreasmolnardev Oct 23, 2025
e6b1286
require at least two pages to show pages tabs
andreasmolnardev Oct 23, 2025
26339a7
document improvements in docs/Releases.md
andreasmolnardev Oct 23, 2025
a770e8f
Merge pull request #45 from andreasmolnardev/jobs-status-monitoring
andreasmolnardev Oct 23, 2025
b8b4127
Merge pull request #43 from m4sc0/5-filter-credentials-from-api-config
andreasmolnardev Oct 23, 2025
d2323c8
improved type checking
m4sc0 Oct 23, 2025
57e4765
Merge pull request #46 from m4sc0/5-filter-credentials-from-api-config
andreasmolnardev Oct 23, 2025
8e4e00f
mask icons dynamically
andreasmolnardev Oct 23, 2025
c659622
return empty div intead of null
andreasmolnardev Oct 23, 2025
3fd7f5a
edit releases.me
andreasmolnardev Oct 23, 2025
487f3bf
Merge pull request #47 from andreasmolnardev/jobs-status-monitoring
andreasmolnardev Oct 23, 2025
b3ad572
feat/show accent color on button
m4sc0 Oct 23, 2025
ba71491
Merge pull request #48 from m4sc0/8-settings-show-current-accent-colo…
andreasmolnardev Oct 24, 2025
c96c9dc
Refactor old wallpaper retrieval logic
andreasmolnardev Oct 24, 2025
d974f18
Merge pull request #49 from andreasmolnardev/wallpaper-patch
andreasmolnardev Oct 24, 2025
1d59381
Fix icon display and wallpaper upload issues
andreasmolnardev Oct 24, 2025
95cfc9c
gitignore: add jobs node modules
andreasmolnardev Oct 25, 2025
e7b142a
fix accent color select outline
andreasmolnardev Oct 25, 2025
73478c1
fix accent color select outline from PR#50
andreasmolnardev Oct 25, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
#pocketbase data
/pocketbase/pb_data

#jobs runner's node modules
/jobs/node_modules

# next.js
/.next/
/out/
Expand Down
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ This is my attempt to solving that.
<img width="1445" height="827" alt="Screenshot 2025-10-22 at 08 03 40" src="https://github.com/user-attachments/assets/69061ca0-cba1-4c23-b7bd-59ca691507e0" />

## Features
- Links: store your most important links for quick access
- Glanceables: Bits of one-line information next to the clock
- GUI Editing: Edit and manage links, search engines, your wallpapers in settings via the GUI instead of relying purely on a config.
- Built-in Authentication: dashwise has Auth built right into it - and even features
- SSO via OIDC: While tested with PocketId, it should work via OIDC - which can be configured in Pocketbase directly.
- Links: store your most important links for quick access and group them into Link Groups. Folders coming soon.
- Glanceables: Customizable bits of one-line information next to the clock.
- Wallpapers: Upload them to dashwise directly, or even change the default one for new users by mounting one into the container
- Spotlight-like Search: Hit Ctrl+K from your dashboard, and you'll be able to search your links and integrations or use bangs for search engines specified in settings.
- Integrations: directly integrates with your favourite self hosted apps. For now only Karakeep is supported but more integrations are planned.

Expand Down
2 changes: 1 addition & 1 deletion app/(config-wrapper)/settings/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default function AccountSettingsPage() {
<div className="content grid grid-cols-[auto_1fr_auto] font-medium gap-2 items-center">
<section className="frosted flex rounded-lg justify-center col-span-full p-2 items-center gap-6">
<FontAwesomeIcon icon={faCircleUser} className="text-4xl" />
<span>Lorem, ipsum.</span>
<span>{JSON.parse(localStorage.getItem('pb_user') ?? "").name ?? 'Lorem ipsum'}</span>
</section>

<h2 className="text-xl col-span-full">Authentication</h2>
Expand Down
18 changes: 17 additions & 1 deletion app/api/v1/auth/signup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { promises as fs } from 'fs';

export async function POST(request: Request) {
try {
const { email, password, passwordConfirm } = await request.json();
const { name, email, password, passwordConfirm } = await request.json();

if (!email || !password || !passwordConfirm) {
return new NextResponse(JSON.stringify({ error: 'All fields are required' }), {
Expand All @@ -21,10 +21,26 @@ export async function POST(request: Request) {
});
}

// using localpart of email as fallback
let newName;
if (name === "" || !name && typeof email === "string") {
const localPart = email.split("@")[0];
// a little transformation can't be missing
newName = localPart
.replace(/[._-]+/g, ' ')
.trim()
.split(' ')
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) // capitalize
.join(' ');
} else {
newName = name;
}

const pb = getServerPB();

// 1. Create the user
const user = await pb.collection('users').create({
newName,
email,
password,
passwordConfirm,
Expand Down
17 changes: 16 additions & 1 deletion app/api/v1/config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,29 @@ export async function GET(request: Request) {
if (error instanceof ClientResponseError && error.status === 401) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

throw error;
}

const configRecord = await pb.collection('userConfig').getFirstListItem(
`associatedUserId="${authModel.record.id}"`
);

// convert credentials to a boolean list
// integrations: { karakeep: {...} } -> integrations: { karakeep: true/false }
const strippedIntegrations = Object.fromEntries(
Object.entries(configRecord.config.integrations).map(([k, v]) => [
k,
// v needs special type checking
!!v && !Array.isArray(v) &&
typeof v === "object" &&
Object.keys(v).length > 0
])
)

// replace original integrations
configRecord.config.integrations = strippedIntegrations;

return NextResponse.json(configRecord.config);
} catch (error) {
console.error('Error fetching config:', error);
Expand Down
6 changes: 5 additions & 1 deletion app/api/v1/wallpapers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ export async function POST(request: Request) {
uploadForm.append('userId', userId);

// 6) get old wallpaper
let old_wallpaper = await pb.collection('wallpaperStore').getFirstListItem(`userId="${userId}"`);
// 6) get old wallpaper
const userWallpapers = await pb.collection('wallpaperStore').getList(1, 1, {
filter: `userId="${userId}"`,
});

const old_wallpaper = userWallpapers.items?.[0] ?? null;
// 7) create record in PB
const record = await pb.collection('wallpaperStore').create(uploadForm);

Expand Down
2 changes: 1 addition & 1 deletion components/PagesTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function PagesTabs() {
const router = useRouter();
const [selectedPage, setSelectedPage] = useState(config?.pages?.[0] || "");

if (!config?.pages) return null;
if (!config?.pages || config?.pages.length < 2) return <div></div>;

return (
<nav className="flex gap-2 items-center justify-center">
Expand Down
13 changes: 13 additions & 0 deletions components/auth/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { faCircleCheck, faExclamationTriangle } from "@fortawesome/free-solid-sv
import { setTimeout } from "timers"

export default function SignupCard() {
const [name, setName] = useState("");
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
Expand Down Expand Up @@ -46,6 +47,7 @@ export default function SignupCard() {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
email,
password,
passwordConfirm: confirmPassword,
Expand All @@ -61,6 +63,7 @@ export default function SignupCard() {

setTimeout(() => {
// Clear the form fields
setName("")
setEmail("")
setPassword("")
setConfirmPassword("")
Expand Down Expand Up @@ -106,6 +109,16 @@ export default function SignupCard() {
</Alert>
)}

<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
placeholder="John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
Expand Down
22 changes: 16 additions & 6 deletions components/settings/AccentColorSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React, { useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
import { faEyeDropper, faPaintBrush } from "@fortawesome/free-solid-svg-icons";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
Expand Down Expand Up @@ -37,6 +37,10 @@ export default function AccentColorSelectComponent({ className }: { className?:
"#06B6D4",
];

const isCustomAccent = !PRESET_COLORS.some(
(c) => c.toLowerCase() === (accent ?? "").toLowerCase()
);

async function updateAccentColor(newColor: string) {
const color_hex = newColor.startsWith("#") ? newColor : `#${newColor}`;

Expand Down Expand Up @@ -108,17 +112,23 @@ export default function AccentColorSelectComponent({ className }: { className?:
title={c}
aria-label={`Choose ${c}`}
onClick={() => updateAccentColor(c)}
className={`w-7 h-7 rounded-full border-2 transform transition-transform duration-150 active:scale-90 ${
accent?.toLowerCase() === c.toLowerCase() ? "ring-1 ring-offset-1" : ""
}`}
className={`w-7 h-7 rounded-full border-2 transform transition-transform duration-150 active:scale-90 ${accent?.toLowerCase() === c.toLowerCase() ? "ring-1 ring-offset-1" : ""
}`}
style={{ background: c, borderColor: "rgba(255,255,255,0.08)" }}
/>
))}

<span className="w-2 h-2 mx-2 rounded-full frosted"></span>

<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-7 ml-2 px-2 frosted rounded-full">
Customize
<Button
variant="ghost"
className={`frosted rounded-full w-8 h-8 outline-none shadow-none hover:ring-2 hover:ring-gray-300 hover:text-gray-300
transition-all duration-150 ${isCustomAccent ? "ring-2" : ""}`}
style={{ background: accent}}
>
<FontAwesomeIcon icon={faEyeDropper} fontSize={10} />
</Button>
</DropdownMenuTrigger>

Expand Down
50 changes: 26 additions & 24 deletions components/settings/GlanceablePropertiesSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useConfig } from "@/context/ConfigContext";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
import { Checkbox } from "../ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select.tsx";

type Glanceable = {
type: string;
Expand Down Expand Up @@ -213,18 +214,18 @@ function PropertyInput({
? (Intl as any).supportedValuesOf("timeZone")
: [];
return (
<select
value={(value as string) || ""}
onChange={(e) => onChange(e.target.value)}
className="border rounded p-1 w-full"
>
<option value="">Select timezone</option>
{tzs.map((tz) => (
<option key={tz} value={tz}>
{tz}
</option>
))}
</select>
<Select value={(value as string) || ""} onValueChange={onChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select timezone" />
</SelectTrigger>
<SelectContent>
{tzs.map((tz) => (
<SelectItem key={tz} value={tz}>
{tz}
</SelectItem>
))}
</SelectContent>
</Select>
);
}

Expand All @@ -251,18 +252,19 @@ function PropertyInput({
const options = schema.split("|");
const selectedValue = (value as string) ?? options[0];
return (
<select
value={selectedValue}
onChange={(e) => onChange(e.target.value)}
className="border rounded p-1 w-full frosted"
>
{options.map((opt) => (
<option key={opt} value={opt}>
{opt.toUpperCase()}
</option>
))}
</select>
);
<Select value={selectedValue} onValueChange={onChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder={options[0].toUpperCase()} />
</SelectTrigger>
<SelectContent>
{options.map((opt) => (
<SelectItem key={opt} value={opt}>
{opt.toUpperCase()}
</SelectItem>
))}
</SelectContent>
</Select>
)
}

if (isBool) {
Expand Down
Loading