Repository: codewec/dashlit Branch: main Commit: feddf5b3d916 Files: 54 Total size: 51.5 KB Directory structure: gitextract_nb5g7m7j/ ├── .env.example ├── .github/ │ └── workflows/ │ ├── docs.yml │ ├── push-main.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs/ │ ├── .gitignore │ ├── .vitepress/ │ │ ├── config.ts │ │ └── utils/ │ │ └── index.ts │ ├── CNAME │ ├── changelog.md │ ├── contributing.md │ ├── guide/ │ │ ├── getting-started.md │ │ └── what-is.md │ ├── index.md │ └── license.md ├── package.json ├── src/ │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ ├── lib/ │ │ ├── components/ │ │ │ ├── actionButtons.svelte │ │ │ ├── dashboard.svelte │ │ │ ├── emptyGroup.svelte │ │ │ ├── emptyItem.svelte │ │ │ ├── header.svelte │ │ │ ├── modalDelete.svelte │ │ │ ├── modalFormGroup.svelte │ │ │ └── modalFormItem.svelte │ │ ├── factory.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── server/ │ │ │ └── helper.ts │ │ ├── styles/ │ │ │ └── dnd.css │ │ └── types.ts │ └── routes/ │ ├── (auth)/ │ │ ├── login/ │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ └── logout/ │ │ └── +page.server.ts │ ├── +error.svelte │ ├── +layout.server.ts │ ├── +layout.svelte │ ├── +page.server.ts │ ├── +page.svelte │ └── custom.css/ │ └── +server.ts ├── static/ │ └── site.webmanifest ├── svelte.config.js ├── tsconfig.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .env.example ================================================ PASSWORD="password" SECRET_KEY="6ft0ryZAeb3DdFIeEwi4uv5zI69GE2ez" ================================================ FILE: .github/workflows/docs.yml ================================================ name: Deploy Documentation site to GitHub Pages on: push: paths: - 'docs/**' - '.github/workflows/docs.yml' - '*.md' branches: - main workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 10 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - name: Setup Pages uses: actions/configure-pages@v5 - name: Install dependencies run: pnpm install - name: Build with VitePress run: | pnpm run docs:build touch docs/.vitepress/dist/.nojekyll - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: name: github-pages path: docs/.vitepress/dist deploy: name: Deploy needs: build permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/push-main.yml ================================================ name: Push Main env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} on: push: paths-ignore: - 'docs/**' - '.md' branches: - main pull_request: paths-ignore: - 'docs/**' - '*.md' branches: - main jobs: build-and-push-image: name: Push Docker image to GitHub Packages runs-on: ubuntu-latest permissions: packages: write contents: read steps: - name: Check out the repo uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Docker Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 ================================================ FILE: .github/workflows/release.yml ================================================ name: Release Docker Image env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} on: push: tags: - 'v*.*.*' jobs: build-and-push-image: name: Push Docker image to GitHub Packages runs-on: ubuntu-latest permissions: packages: write contents: read steps: - name: Check out the repo uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Docker Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64,linux/arm64 ================================================ FILE: .gitignore ================================================ node_modules # Output .output .vercel .netlify .wrangler /.svelte-kit /build /data # OS .DS_Store Thumbs.db # Env .env .env.* !.env.example !.env.test !.env.ci # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* ================================================ FILE: .npmrc ================================================ engine-strict=true ================================================ FILE: .prettierrc ================================================ { "useTabs": true, "singleQuote": true, "trailingComma": "none", "printWidth": 100, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "overrides": [ { "files": "*.svelte", "options": { "parser": "svelte" } } ] } ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## v0.0.6 [compare changes](https://github.com/codewec/dashlit/compare/v0.0.5...v0.0.6) ### 🚀 Enhancements - Icon custom color ([5e4fc05](https://github.com/codewec/dashlit/commit/5e4fc05)) - Add select url target ([b886d5d](https://github.com/codewec/dashlit/commit/b886d5d)) - Show URL options ([a8e5e10](https://github.com/codewec/dashlit/commit/a8e5e10)) - Check valid ORIGIN ([2e6cf56](https://github.com/codewec/dashlit/commit/2e6cf56)) ### 🩹 Fixes - Changelogen params ([6ae906e](https://github.com/codewec/dashlit/commit/6ae906e)) ### ❤️ Contributors - Wec ## v0.0.5 [compare changes](https://github.com/codewec/dashlit/compare/0.0.4...v0.0.5) ### 🩹 Fixes - GH workflow ([2b23083](https://github.com/codewec/dashlit/commit/2b23083)) ### ❤️ Contributors - Wec ================================================ FILE: Dockerfile ================================================ FROM node:22 AS builder WORKDIR /app COPY . . RUN mv .env.example .env RUN corepack enable && corepack prepare pnpm@latest --activate RUN pnpm install --frozen-lockfile --force RUN npm run build # second stage FROM node:22-alpine WORKDIR /app COPY --from=builder /app/build ./build COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json . EXPOSE 3000 CMD ["sh","-c","node build"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 codewec Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

DashLit

DashLit is a simple, self-hosted Startpage solution. It’s incredibly easy to set up and use, and its built-in editors let you quickly create your own application hub – even with a convenient drag-and-drop interface. You don’t even need to edit any files!

DashLit

Current Version Last commit License MIT

DashLit

## 🚀 Getting started ### Docker This Docker image is published on the GitHub container registry - `ghcr.io/codewec/dashlit`. #### Minimal configuration without password ```yaml services: app: container_name: dashlit-app image: ghcr.io/codewec/dashlit:latest restart: unless-stopped environment: ORIGIN: '${ORIGIN:-http://localhost:3000}' # please provide URL if different ports: - '3000:3000' volumes: - ./data:/app/data ``` #### Full configuration with password ```yaml services: app: container_name: dashlit-app image: ghcr.io/codewec/dashlit:latest environment: ORIGIN: '${ORIGIN:-http://localhost:3000}' # please provide URL if different NODE_ENV: '${NODE_ENV:-production}' # optional for production environment HOST_HEADER: '${HOST_HEADER:-HOST}' # optional for nginx reverse proxy ADDRESS_HEADER: '${ADDRESS_HEADER:-X-Real-IP}' # optional for nginx reverse proxy PROTOCOL_HEADER: '${PROTOCOL_HEADER:-X-Forwarded-Proto}' # optional for nginx reverse proxy PASSWORD: '${PASSWORD:-password}' SECRET_KEY: '${SECRET_KEY:-any-secret-string-for-jwt-auth}' # optional key for JWT authentication restart: unless-stopped ports: - '3000:3000' volumes: - ./data:/app/data ``` ================================================ FILE: docker-compose.yml ================================================ services: app: container_name: dashlit-app image: ghcr.io/codewec/dashlit:latest restart: unless-stopped environment: ORIGIN: '${ORIGIN:-http://localhost:3000}' # please provide URL if different ports: - '3000:3000' volumes: - ./data:/app/data ================================================ FILE: docs/.gitignore ================================================ .vitepress/cache .vitepress/dist ================================================ FILE: docs/.vitepress/config.ts ================================================ import { defineConfig } from 'vitepress'; import { getVersion } from './utils'; // https://vitepress.dev/reference/site-config export default defineConfig({ title: 'DashLit', description: 'DashLit - A simple solution for self-hosting your home page.', head: [['link', { rel: 'icon', href: '/favicon.png' }]], themeConfig: { search: { provider: 'local' }, logo: { src: '/logo.png', innerWidth: 50, height: 50 }, nav: [ { text: 'Home', link: '/' }, { text: 'Getting Started', link: '/guide/getting-started' }, { text: 'Demo', link: 'https://demo.dashlit.cwec.dev' }, { text: getVersion(), items: [ { text: 'Changelog', link: '/changelog' }, { text: 'Contributing', link: '/contributing' } ] } ], socialLinks: [{ icon: 'github', link: 'https://github.com/codewec/dashlit' }], sidebar: [ { text: 'Guide', base: '/guide', items: [ { text: 'What is DashLit?', link: '/what-is' }, { text: 'Getting Started', link: '/getting-started' } ] }, { text: 'Contributing', link: '/contributing' }, { text: 'Changelog', link: '/changelog' }, { text: 'License', link: '/license' } ], editLink: { pattern: 'https://github.com/codewec/dashlit/edit/main/docs/:path', text: 'Edit this page on GitHub' }, lastUpdated: { text: 'Last updated', formatOptions: { dateStyle: 'short', timeStyle: 'medium' } }, footer: { message: 'Released under the MIT License.', copyright: 'Copyright © 2025 CodeWec' } } }); ================================================ FILE: docs/.vitepress/utils/index.ts ================================================ import currentPackage from '../../../package.json'; export function getVersion() { return currentPackage.version || '0.0.0'; } ================================================ FILE: docs/CNAME ================================================ dashlit.cwec.dev ================================================ FILE: docs/changelog.md ================================================ --- search: false --- ================================================ FILE: docs/contributing.md ================================================ # Contributing First of all, thank you for deciding to contribute to the project. There are several ways to help the project develop. ## Bug fix Please note that occasional bugs or issues may arise when using this application – this is standard practice. Given your knowledge of JavaScript, you may be able to resolve the problem yourself. Alternatively, please [report the issue](https://github.com/codewec/dashlit/issues/new?assignees=codewec&labels=bug&projects=&template=bug.yml&title=%5BBUG%5D+%3Ctitle%3E) and I’ll be happy to assist you. ## Star on Github The simplest thing you can do is leave us a star on [Github](https://github.com/codewec/dashlit) – it only takes a few seconds, and I really appreciate it! ================================================ FILE: docs/guide/getting-started.md ================================================ # 🚀 Getting started ================================================ FILE: docs/guide/what-is.md ================================================ # What is DashLit? `DashLit` is a flexible tool for creating homepages, especially useful for those managing their own server and services. It helps you collect and organize all your links in one place. `DashLit` simplifies creating and managing your own online services. It boasts a user-friendly drag-and-drop interface, eliminating the need for complex configuration files like YAML. All service management is handled directly through the intuitive web interface. Plus, `DashLit` offers secure authentication, making it ideal for deploying your services publicly online with confidence. ![DashLit](/main_page.png) ================================================ FILE: docs/index.md ================================================ --- layout: home hero: name: "DashLit" text: "Personal home page" tagline: A simple solution for self-hosting your home page. image: src: /logo.png alt: DashLit actions: - theme: brand text: Get Started link: /guide/getting-started - theme: alt text: Live Demo link: https://demo.dashlit.cwec.dev - theme: alt text: View on GitHub link: https://github.com/codewec/dashlit features: - title: Privacy icon: 🔐 details: Offers secure authentication. - title: Themes icon: 🌗 details: Enjoy a light or dark theme – your choice! - title: Grouping icon: 🗂 details: Create custom service groups. - title: Easy setup icon: 👌 details: Does not use manual configuration files. - title: Drag and drop icon: ✨ details: Quickly organize links with a simple drag-and-drop interface. - title: Docker icon: 🐳 details: Optimized docker images for popular platforms. - title: Free icon: 🚀 details: Dashlit is completely free and open source. - title: PWA icon: 📲 details: Installable application. --- ## Screenshot ![DashLit](/main_page.png "DashLit") ================================================ FILE: docs/license.md ================================================ # License ================================================ FILE: package.json ================================================ { "name": "dashlit", "private": true, "version": "0.0.6", "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "prepare": "svelte-kit sync || echo ''", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "release": "vite build && changelogen --hideAuthorEmail --release --push" }, "devDependencies": { "@iconify/svelte": "^5.0.0", "@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-node": "^5.2.12", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/vite": "^4.0.0", "@types/node": "^24.0.1", "changelogen": "^0.6.1", "flowbite": "^3.1.2", "flowbite-svelte": "^1.6.4", "flowbite-svelte-icons": "^2.2.0", "postcss": "^8.5.4", "prettier": "^3.5.3", "prettier-plugin-svelte": "^3.4.0", "prettier-plugin-tailwindcss": "^0.6.12", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "tailwindcss": "^4.0.0", "typescript": "^5.0.0", "vite": "^6.2.6", "vitepress": "^1.6.3" }, "pnpm": { "onlyBuiltDependencies": [ "@tailwindcss/oxide", "esbuild" ] }, "dependencies": { "@thisux/sveltednd": "^0.0.20", "dotenv": "^16.6.0", "jose": "^6.0.11", "svelte-5-french-toast": "^2.0.4" } } ================================================ FILE: src/app.css ================================================ @import 'tailwindcss'; @plugin '@tailwindcss/forms'; @plugin 'flowbite/plugin'; @custom-variant dark (&:where(.dark, .dark *)); @theme { --color-primary-50: #fff5f2; --color-primary-100: #fff1ee; --color-primary-200: #ffe4de; --color-primary-300: #ffd5cc; --color-primary-400: #ffbcad; --color-primary-500: #fe795d; --color-primary-600: #ef562f; --color-primary-700: #eb4f27; --color-primary-800: #cc4522; --color-primary-900: #a5371b; --color-secondary-50: #f0f9ff; --color-secondary-100: #e0f2fe; --color-secondary-200: #bae6fd; --color-secondary-300: #7dd3fc; --color-secondary-400: #38bdf8; --color-secondary-500: #0ea5e9; --color-secondary-600: #0284c7; --color-secondary-700: #0369a1; --color-secondary-800: #075985; --color-secondary-900: #0c4a6e; } @source "../node_modules/flowbite-svelte/dist"; @source "../node_modules/flowbite-svelte-icons/dist"; @layer base { /* disable chrome cancel button */ input[type='search']::-webkit-search-cancel-button { display: none; } } .toaster { .wrapper { .base { @apply bg-gray-100 dark:bg-slate-900 dark:text-gray-400; } } } ================================================ FILE: src/app.d.ts ================================================ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { namespace App { interface Locals { userAuthenticated: boolean; } // interface Error {} // interface Locals {} // interface PageData {} // interface PageState {} // interface Platform {} } } export {}; ================================================ FILE: src/app.html ================================================ %sveltekit.head%
%sveltekit.body%
================================================ FILE: src/hooks.server.ts ================================================ import type { Handle } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; import * as jose from 'jose'; import { hashString } from '$lib/helpers'; import { cookie_token_key } from '$lib'; import { getSecretKey } from '$lib/server/helper'; export const handle: Handle = async ({ event, resolve }) => { if (!env.PASSWORD || env.PASSWORD?.length === 0) { event.locals.userAuthenticated = true; return resolve(event); } const token = event.cookies.get(cookie_token_key); if (!token) { return resolve(event); } const secret = await getSecretKey(env.PASSWORD); await jose .jwtVerify(token, new TextEncoder().encode(secret)) .then(() => { event.locals.userAuthenticated = true; }) .catch(() => { event.cookies.delete(cookie_token_key, { path: '/' }); }); return resolve(event); }; ================================================ FILE: src/lib/components/actionButtons.svelte ================================================
================================================ FILE: src/lib/components/dashboard.svelte ================================================
{#if editMode} handleClickGroupAction(ActionType.CREATE, newGroup())} /> {/if} {#each groups as group (`g_${group.id}`)}
(disableItemDrag = true), onDragEnd: () => (disableItemDrag = false) } }} use:droppable={{ dragData: group, container: `${group.id}`, disabled: isDisabledGroupDrop(group), callbacks: { onDrop: onDropInGroup } }} >

{group.title}

{#if group.description}

{group.description}

{/if}
{#if editMode} { hoveredOnActionsEnitytId = id; }} handleClick={(action) => handleClickGroupAction(action, group)} /> {/if}
{#each group.items as item (`i_${item.id}`)}
{ if (e.key === 'Enter') { handleClickItem(item); } }} onmouseenter={() => { if (editMode) { hoveredItemId = undefined; return; } hoveredItemId = `${group.id}-${item.id}`; }} onmouseleave={() => { hoveredItemId = undefined; }} onclick={() => { handleClickItem(item); }} use:draggable={{ container: `${group.id}-${item.id}`, dragData: item, disabled: isDisabledItemDrag(`${group.id}-${item.id}`), callbacks: { onDragStart: () => (disableGroupsDrag = true), onDragEnd: () => (disableGroupsDrag = false) } }} use:droppable={{ dragData: item, disabled: disableItemDrag, container: `${group.id}-${item.id}`, callbacks: { onDrop: onDropInItem } }} animate:flip={{ duration: 200 }} in:fade={{ duration: 150 }} out:fade={{ duration: 150 }} class="item svelte-dnd-touch-feedback" >
{#if item.icon}
{#if isUrlString(item.icon)} {item.title} {:else} {/if}
{/if}

{item.title}

{#if getDescription(group.id, item)}

{getDescription(group.id, item)}

{/if} {#if getUrl(item)}

{getUrl(item)}

{/if}
{#if editMode} { hoveredOnActionsEnitytId = id; }} handleClick={(action) => handleClickItemAction(action, group.id, item)} /> {/if}
{/each} {#if editMode} { hoveredOnActionsEnitytId = id; }} handleClick={() => handleClickItemAction(ActionType.CREATE, group.id, newItem())} /> {/if}
{/each}
================================================ FILE: src/lib/components/emptyGroup.svelte ================================================ ================================================ FILE: src/lib/components/emptyItem.svelte ================================================ ================================================ FILE: src/lib/components/header.svelte ================================================

DashLit

{#if editMode} handleSave()}>Save {:else} {/if} {#if canLogout} {/if}
================================================ FILE: src/lib/components/modalDelete.svelte ================================================ handleClose()} transition={slide} size="xs"> {#if entity}

Are you sure you want to delete "{entity.element.title}"?

{/if}
================================================ FILE: src/lib/components/modalFormGroup.svelte ================================================ handleClose(undefined)} transition={slide} size="xs">
{ e.preventDefault(); handleClose(form); }} >
================================================ FILE: src/lib/components/modalFormItem.svelte ================================================ handleClose(undefined)} transition={slide} size="xs">
{ e.preventDefault(); handleClose(form); }} >
URL or Icon name from Iconify. The color is applied only to the Iconify icon.
================================================ FILE: src/lib/factory.ts ================================================ import { generateRandomString } from './helpers'; import type { Group, Item } from './types'; export const newGroup = (): Group => { return { id: generateRandomString(10), title: '', items: [] }; }; export const newItem = (): Item => { return { id: generateRandomString(10), title: '', description: '', url: '' }; }; ================================================ FILE: src/lib/helpers.ts ================================================ import type { Ids } from './types'; export const hasField = (data: any, key: string): data is T => { return key in data; }; export const getIds = (str: string): Ids | undefined => { const parts = str.split('-'); if (parts.length !== 2) { return undefined; } return { groupId: parts[0], itemId: parts[1] }; }; export const hashString = async (message: string): Promise => { const msgBuffer = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); return hashHex; }; export const generateRandomString = (length: number) => { return Math.random() .toString(36) .substring(2, 2 + length); }; export const isUrlString = (str: string): boolean => { if (!str) { return false; } const urlRegex = /^(ftp|http|https):\/\/[^ "]+$/; return urlRegex.test(str); }; ================================================ FILE: src/lib/index.ts ================================================ export const page_title = 'Dashlit'; export const default_dashboard = 'dashboard.json'; export const data_path = '/app/data'; export const cookie_token_key = 'token'; ================================================ FILE: src/lib/server/helper.ts ================================================ import { env } from '$env/dynamic/private'; import { hashString } from '$lib/helpers'; import currentPackage from '../../../package.json'; import { default_dashboard, data_path } from '$lib'; export const getSecretKey = async (password: string) => { return (env.SECRET_KEY?.length ?? 0 > 0) ? env.SECRET_KEY : await hashString(password); }; export const getVersion = () => { return currentPackage.version || '0.0.0'; }; export const dataPath = () => { return env.DATA_PATH ?? data_path; }; export const filePath = () => { return `${dataPath()}/${default_dashboard}`; }; ================================================ FILE: src/lib/styles/dnd.css ================================================ /* Base draggable styles */ .svelte-dnd-draggable { touch-action: none; /* Prevents touch scrolling while dragging */ user-select: none; /* Prevents text selection during drag */ } /* Active dragging state */ .svelte-dnd-dragging { opacity: 0.5; cursor: grabbing; } /* Draggable hover state */ .svelte-dnd-draggable:hover { cursor: grab; } /* Droppable area styles */ .svelte-dnd-droppable { position: relative; } /* Active drop target */ .svelte-dnd-drop-target { outline: 2px dashed #4caf50; } /* Invalid drop target */ .svelte-dnd-invalid-target { outline: 2px dashed #f44336; } /* Drop preview/placeholder */ .svelte-dnd-placeholder { border: 2px dashed #9e9e9e; } /* Media queries for responsive design */ @media (max-width: 600px) { .svelte-dnd-draggable { width: 100%; touch-action: none; /* Prevents scrolling during drag */ } .svelte-dnd-droppable { padding: 10px; } } ================================================ FILE: src/lib/types.ts ================================================ export enum ActionType { CREATE = 'create', DELETE = 'delete', EDIT = 'edit' } export enum ShowUrlType { NEVER = 'never', ALWAYS = 'always', DESC_EMPTY = 'empty_desc', HOVER = 'hover' } export interface Item { id: string; title: string; url: string; showUrl?: ShowUrlType; target?: string; description?: string; icon?: string; iconColor?: string; } export interface Group { id: string; title: string; description?: string; items: Item[]; } export interface Dashboard { version: string; groups: Group[]; } export interface Ids { groupId: string; itemId: string; } export type DeletionEntity = { ids: Ids; element: Group | Item; }; export type EditableItem = { groupId: string; item: Item; }; ================================================ FILE: src/routes/(auth)/login/+page.server.ts ================================================ import type { PageServerLoad } from './$types'; import { fail, redirect } from '@sveltejs/kit'; import { env } from '$env/dynamic/private'; import * as jose from 'jose'; import { cookie_token_key } from '$lib'; import { getSecretKey } from '$lib/server/helper'; export const load: PageServerLoad = async (event) => { if (event.locals.userAuthenticated) { return redirect(302, '/'); } return {}; }; export const actions = { login: async ({ request, cookies }) => { const form = await request.formData(); let password = String(form.get('password')); if (!password) { return fail(400, { error: 'Password is required' }); } if (password !== env.PASSWORD) { return fail(401, { error: 'Invalid password' }); } const secretKey = await getSecretKey(password); const sign = new TextEncoder().encode(secretKey); const token = await new jose.SignJWT() .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('4weeks') .sign(sign); cookies.set(cookie_token_key, token, { path: '/', httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production', maxAge: 60 * 60 * 24 * 30 }); redirect(302, '/'); } }; ================================================ FILE: src/routes/(auth)/login/+page.svelte ================================================ {page_title}
{ const toastId = toast.loading('Checking...'); return async ({ result, update }) => { await update(); if (result.type === 'failure') { const message = (result.data?.error as string) ?? 'An error occurred'; toast.error(message, { id: toastId }); } else { toast.success('Welcome back!', { icon: '👋', id: toastId }); } }; }} action="?/login" class="flex flex-col space-y-6" >
================================================ FILE: src/routes/(auth)/logout/+page.server.ts ================================================ import { cookie_token_key } from '$lib'; import type { PageServerLoad } from './$types'; import { redirect } from '@sveltejs/kit'; export const load: PageServerLoad = async (event) => { event.cookies.delete(cookie_token_key, { path: '/' }); event.locals.userAuthenticated = false; return redirect(302, '/login'); }; ================================================ FILE: src/routes/+error.svelte ================================================ {page.status} - error ================================================ FILE: src/routes/+layout.server.ts ================================================ import type { LayoutServerLoad } from './$types'; import { env } from '$env/dynamic/private'; export const load: LayoutServerLoad = async ({ locals }) => { return { envOrigin: env.ORIGIN ?? '', userAuthenticated: locals.userAuthenticated }; }; ================================================ FILE: src/routes/+layout.svelte ================================================ {#if data.envOrigin !== page.url.origin}

Invalid ORIGIN found. Please specify ORIGIN={page.url.origin}. Check the Docs.

{/if} {@render children()} ================================================ FILE: src/routes/+page.server.ts ================================================ import type { PageServerLoad } from './$types'; import { fail, redirect } from '@sveltejs/kit'; import fs from 'node:fs/promises'; import { env } from '$env/dynamic/private'; import type { Dashboard } from '$lib/types'; import { dataPath, filePath, getVersion } from '$lib/server/helper'; export const load: PageServerLoad = async (event) => { if (!event.locals.userAuthenticated) { return redirect(302, '/login'); } const data = await fs.readFile(filePath(), { encoding: 'utf8' }).catch(() => { console.log(`File ${filePath()} not found`); return '{}'; }); const dashboard: Dashboard = JSON.parse(data); return { groups: dashboard.groups, canLogout: (env.PASSWORD?.length ?? 0) > 0, isDemoMode: env.DEMO_MODE === 'true' }; }; export const actions = { default: async ({ locals, request }) => { if (!locals.userAuthenticated) { return redirect(302, '/login'); } const json = await request.json(); await fs.mkdir(dataPath(), { recursive: true }).catch(console.error); await fs .writeFile(filePath(), JSON.stringify({ version: getVersion(), groups: json })) .catch((error) => { console.log(`Cant write ${filePath()}`); return fail(500); }); return { status: 'ok' }; } }; ================================================ FILE: src/routes/+page.svelte ================================================ {page_title}
(editMode = !editMode)} />
(deletionEntity = undefined)} handleConfirm={handleDeleteEntity} /> { if (group) { handleSaveGroup(group); } else { editableGroup = undefined; } }} /> { if (item && editableItem) { handleSaveItem(editableItem.groupId, item); } else { editableItem = undefined; } }} /> ================================================ FILE: src/routes/custom.css/+server.ts ================================================ import { dataPath, filePath } from '$lib/server/helper'; import { type RequestHandler } from '@sveltejs/kit'; import fs from 'node:fs/promises'; export const GET: RequestHandler = async ({ url }) => { const fileName = `${dataPath()}/custom.css`; const data = await fs.readFile(fileName, { encoding: 'utf8' }).catch(() => { console.log(`File ${fileName} not found`); return ''; }); return new Response(String(data), { headers: { 'Content-type': 'text/css' } }); }; ================================================ FILE: static/site.webmanifest ================================================ { "name": "DashLit", "short_name": "DashLit", "icons": [ { "src": "/web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: svelte.config.js ================================================ import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ out: 'build', precompress: true, envPrefix: '' }) } }; export default config; ================================================ FILE: tsconfig.json ================================================ { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "moduleResolution": "bundler" } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes // from the referenced tsconfig.json - TypeScript does not merge them in } ================================================ FILE: vite.config.ts ================================================ import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [tailwindcss(), sveltekit()], server: { allowedHosts: true } });