├── .npmrc ├── netlify.toml ├── static ├── favicon.png └── no_cover.png ├── .prettierignore ├── src ├── lib │ ├── components │ │ ├── ui │ │ │ ├── sonner │ │ │ │ ├── index.ts │ │ │ │ └── sonner.svelte │ │ │ ├── progress │ │ │ │ ├── index.ts │ │ │ │ └── progress.svelte │ │ │ ├── skeleton │ │ │ │ ├── index.ts │ │ │ │ └── skeleton.svelte │ │ │ ├── dialog │ │ │ │ ├── dialog-portal.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── index.ts │ │ │ │ └── dialog-content.svelte │ │ │ ├── card │ │ │ │ ├── card-content.svelte │ │ │ │ ├── card-footer.svelte │ │ │ │ ├── card-header.svelte │ │ │ │ ├── card-description.svelte │ │ │ │ ├── card.svelte │ │ │ │ ├── card-title.svelte │ │ │ │ └── index.ts │ │ │ ├── pagination │ │ │ │ ├── pagination-item.svelte │ │ │ │ ├── pagination-content.svelte │ │ │ │ ├── pagination-ellipsis.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── pagination-next-button.svelte │ │ │ │ ├── pagination-prev-button.svelte │ │ │ │ ├── pagination-link.svelte │ │ │ │ └── pagination.svelte │ │ │ ├── select │ │ │ │ ├── select-separator.svelte │ │ │ │ ├── select-label.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── select-trigger.svelte │ │ │ │ ├── select-content.svelte │ │ │ │ └── select-item.svelte │ │ │ ├── table │ │ │ │ ├── table-body.svelte │ │ │ │ ├── table-caption.svelte │ │ │ │ ├── table-footer.svelte │ │ │ │ ├── table-cell.svelte │ │ │ │ ├── table.svelte │ │ │ │ ├── table-head.svelte │ │ │ │ ├── table-header.svelte │ │ │ │ ├── table-row.svelte │ │ │ │ └── index.ts │ │ │ ├── badge │ │ │ │ ├── badge.svelte │ │ │ │ └── index.ts │ │ │ ├── button │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ └── input │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ ├── GlobalLoading.svelte │ │ ├── BookSkeleton.svelte │ │ ├── ThemeSwitch.svelte │ │ ├── Paginator.svelte │ │ ├── SearchBar.svelte │ │ ├── DownloadProgress.svelte │ │ └── BookUI.svelte │ ├── index.ts │ ├── constants.ts │ ├── types │ │ └── Book.ts │ └── utils.ts ├── app.d.ts ├── store │ ├── globalStore.ts │ └── downloadStore.ts ├── app.html ├── routes │ ├── +page.svelte │ ├── search │ │ ├── +page.server.ts │ │ └── +page.svelte │ └── +layout.svelte └── app.css ├── postcss.config.js ├── vite.config.ts ├── .gitignore ├── .prettierrc ├── components.json ├── tsconfig.json ├── eslint.config.js ├── svelte.config.js ├── README.md ├── package.json └── tailwind.config.ts /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "build" 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henacodes/libros/HEAD/static/favicon.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /static/no_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henacodes/libros/HEAD/static/no_cover.png -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from "./sonner.svelte"; 2 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./progress.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Progress, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./skeleton.svelte"; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Skeleton, 7 | }; 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_SERVER_URL = 'https://libros-server.onrender.com'; //'http://localhost:3000'; 2 | export const NOT_AVAILABLE = 'N/A'; 3 | export const DOWNLOAD_HISTORY = 'download-history'; 4 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-portal.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | /.netlify 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /src/lib/components/GlobalLoading.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "src\\app.css", 7 | "baseColor": "slate" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils" 12 | }, 13 | "typescript": true 14 | } -------------------------------------------------------------------------------- /src/store/globalStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | const globalStore = writable({ loading: false }); 4 | 5 | export const toggleLoading = (state?: boolean) => 6 | globalStore.update((curr) => ({ 7 | ...curr, 8 | loading: typeof state === 'boolean' ? state : !curr.loading 9 | })); 10 | 11 | export default globalStore; 12 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-item.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
  • 12 | 13 |
  • 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/skeleton/skeleton.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 |
    14 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-separator.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/types/Book.ts: -------------------------------------------------------------------------------- 1 | export default interface Book { 2 | id: string; 3 | authors: string; 4 | title: string; 5 | publisher: string; 6 | year: string; 7 | pages: string; 8 | language: string; 9 | size: string; 10 | extension: string; 11 | mirror: string; 12 | thumbUrl?: string; 13 | } 14 | 15 | export interface BookDownload extends Book { 16 | loaded: number; 17 | total: number; 18 | done?: boolean; 19 | date?: Date; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 |
    14 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |

    12 | 13 |

    14 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-body.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-content.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 |
    14 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-caption.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    15 | 16 |
    17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    15 | 16 |
    17 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-cell.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-label.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
    12 | 13 | 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/components/BookSkeleton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 |
    13 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-head.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-header.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    17 |
    18 | 19 |
    20 |
    21 | 22 |
    23 |
    24 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/badge.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/table-row.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-ellipsis.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | More pages 19 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./card.svelte"; 2 | import Content from "./card-content.svelte"; 3 | import Description from "./card-description.svelte"; 4 | import Footer from "./card-footer.svelte"; 5 | import Header from "./card-header.svelte"; 6 | import Title from "./card-title.svelte"; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle, 22 | }; 23 | 24 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 25 | -------------------------------------------------------------------------------- /src/lib/components/ThemeSwitch.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./table.svelte"; 2 | import Body from "./table-body.svelte"; 3 | import Caption from "./table-caption.svelte"; 4 | import Cell from "./table-cell.svelte"; 5 | import Footer from "./table-footer.svelte"; 6 | import Head from "./table-head.svelte"; 7 | import Header from "./table-header.svelte"; 8 | import Row from "./table-row.svelte"; 9 | 10 | export { 11 | Root, 12 | Body, 13 | Caption, 14 | Cell, 15 | Footer, 16 | Head, 17 | Header, 18 | Row, 19 | // 20 | Root as Table, 21 | Body as TableBody, 22 | Caption as TableCaption, 23 | Cell as TableCell, 24 | Footer as TableFooter, 25 | Head as TableHead, 26 | Header as TableHeader, 27 | Row as TableRow, 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/components/ui/progress/progress.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 17 |
    21 |
    22 | -------------------------------------------------------------------------------- /src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./pagination.svelte"; 2 | import Content from "./pagination-content.svelte"; 3 | import Item from "./pagination-item.svelte"; 4 | import Link from "./pagination-link.svelte"; 5 | import PrevButton from "./pagination-prev-button.svelte"; 6 | import NextButton from "./pagination-next-button.svelte"; 7 | import Ellipsis from "./pagination-ellipsis.svelte"; 8 | 9 | export { 10 | Root, 11 | Content, 12 | Item, 13 | Link, 14 | PrevButton, 15 | NextButton, 16 | Ellipsis, 17 | // 18 | Root as Pagination, 19 | Content as PaginationContent, 20 | Item as PaginationItem, 21 | Link as PaginationLink, 22 | PrevButton as PaginationPrevButton, 23 | NextButton as PaginationNextButton, 24 | Ellipsis as PaginationEllipsis, 25 | }; 26 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import prettier from 'eslint-config-prettier'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import tseslint from 'typescript-eslint'; 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | ...svelte.configs['flat/recommended'], 11 | prettier, 12 | ...svelte.configs['flat/prettier'], 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | }, 21 | { 22 | files: ['**/*.svelte'], 23 | languageOptions: { 24 | parserOptions: { 25 | parser: tseslint.parser 26 | } 27 | } 28 | }, 29 | { 30 | ignores: ['build/', '.svelte-kit/', 'dist/'] 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/button.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/routes/search/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { API_SERVER_URL } from '$lib/constants.js'; 2 | 3 | export const load = async ({ url, fetch }) => { 4 | const query = url.searchParams.get('query'); 5 | const filterBy = url.searchParams.get('filterBy'); 6 | const currentPage = url.searchParams.get('page'); 7 | if (!query) { 8 | return new Response(JSON.stringify({ error: 'Query parameter is missing' }), { status: 400 }); 9 | } 10 | 11 | const response = await fetch( 12 | `${API_SERVER_URL}/books/search?query=${query}&filterBy=${filterBy}&page=${currentPage}` 13 | ); 14 | 15 | const serverResponse = await response.json(); 16 | return { 17 | query, 18 | currentPage, 19 | searchResults: serverResponse.data.searchResults, 20 | filterBy, 21 | totalPages: serverResponse.data.totalPages, 22 | error: serverResponse.error 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | import adapter from '@sveltejs/adapter-netlify'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter(), 15 | alias: { 16 | '@/*': './path/to/lib/*', 17 | $store: 'src/store/' 18 | } 19 | } 20 | }; 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-next-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-prev-button.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 27 | 28 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | import { Select as SelectPrimitive } from "bits-ui"; 2 | 3 | import Label from "./select-label.svelte"; 4 | import Item from "./select-item.svelte"; 5 | import Content from "./select-content.svelte"; 6 | import Trigger from "./select-trigger.svelte"; 7 | import Separator from "./select-separator.svelte"; 8 | 9 | const Root = SelectPrimitive.Root; 10 | const Group = SelectPrimitive.Group; 11 | const Input = SelectPrimitive.Input; 12 | const Value = SelectPrimitive.Value; 13 | 14 | export { 15 | Root, 16 | Group, 17 | Input, 18 | Label, 19 | Item, 20 | Value, 21 | Content, 22 | Trigger, 23 | Separator, 24 | // 25 | Root as Select, 26 | Group as SelectGroup, 27 | Input as SelectInput, 28 | Label as SelectLabel, 29 | Item as SelectItem, 30 | Value as SelectValue, 31 | Content as SelectContent, 32 | Trigger as SelectTrigger, 33 | Separator as SelectSeparator, 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, tv } from "tailwind-variants"; 2 | export { default as Badge } from "./badge.svelte"; 3 | 4 | export const badgeVariants = tv({ 5 | base: "focus:ring-ring inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2", 6 | variants: { 7 | variant: { 8 | default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent", 9 | secondary: 10 | "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent", 11 | destructive: 12 | "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent", 13 | outline: "text-foreground", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | }); 20 | 21 | export type Variant = VariantProps["variant"]; 22 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from "./input.svelte"; 2 | 3 | export type FormInputEvent = T & { 4 | currentTarget: EventTarget & HTMLInputElement; 5 | }; 6 | export type InputEvents = { 7 | blur: FormInputEvent; 8 | change: FormInputEvent; 9 | click: FormInputEvent; 10 | focus: FormInputEvent; 11 | focusin: FormInputEvent; 12 | focusout: FormInputEvent; 13 | keydown: FormInputEvent; 14 | keypress: FormInputEvent; 15 | keyup: FormInputEvent; 16 | mouseover: FormInputEvent; 17 | mouseenter: FormInputEvent; 18 | mouseleave: FormInputEvent; 19 | mousemove: FormInputEvent; 20 | paste: FormInputEvent; 21 | input: FormInputEvent; 22 | wheel: FormInputEvent; 23 | }; 24 | 25 | export { 26 | Root, 27 | // 28 | Root as Input, 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination-link.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 | {page.value} 34 | 35 | -------------------------------------------------------------------------------- /src/lib/components/ui/pagination/pagination.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from "bits-ui"; 2 | 3 | import Title from "./dialog-title.svelte"; 4 | import Portal from "./dialog-portal.svelte"; 5 | import Footer from "./dialog-footer.svelte"; 6 | import Header from "./dialog-header.svelte"; 7 | import Overlay from "./dialog-overlay.svelte"; 8 | import Content from "./dialog-content.svelte"; 9 | import Description from "./dialog-description.svelte"; 10 | 11 | const Root = DialogPrimitive.Root; 12 | const Trigger = DialogPrimitive.Trigger; 13 | const Close = DialogPrimitive.Close; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Portal, 19 | Footer, 20 | Header, 21 | Trigger, 22 | Overlay, 23 | Content, 24 | Description, 25 | Close, 26 | // 27 | Root as Dialog, 28 | Title as DialogTitle, 29 | Portal as DialogPortal, 30 | Footer as DialogFooter, 31 | Header as DialogHeader, 32 | Trigger as DialogTrigger, 33 | Overlay as DialogOverlay, 34 | Content as DialogContent, 35 | Description as DialogDescription, 36 | Close as DialogClose, 37 | }; 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to the **Libros**! This project is created by [@Henacodes](https://github.com/henacodes), serves as a learning experience and a platform for collaboration. If you're experienced with **Svelte** and **Tailwind CSS**, I'd love to connect and work together! 4 | 5 | ## How to run the Libros locally 6 | 7 | Make sure you have **Git** and **NodeJS** installed on your computer. 8 | 9 | ```Bash 10 | # Clone the project 11 | git clone https://github.com/henacodes/libros 12 | 13 | # Install npm packages 14 | # first you have to be in the project directory 15 | npm install 16 | 17 | # Run 18 | npm run dev 19 | 20 | # Build before you make a pull request 21 | npm run build 22 | ``` 23 | 24 | ## Contribute 25 | 26 | I welcome contributions! After cloning and running the project locally, feel free to make changes. If everything works as expected, please create a pull request to the main branch. 27 | 28 | ## Thank you! 29 | 30 | Thank you for checking out this project! Your feedback and contributions are greatly appreciated. 31 | -------------------------------------------------------------------------------- /src/store/downloadStore.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import type { BookDownload } from '$lib/types/Book'; 3 | import type Book from '$lib/types/Book'; 4 | 5 | const downloadStore = writable({ downloads: [] } as { downloads: BookDownload[] }); 6 | 7 | export const addDownloadHistory = (books: BookDownload[]) => { 8 | downloadStore.update((curr) => ({ ...curr, downloads: [...curr.downloads, ...books] })); 9 | }; 10 | 11 | export const addDownload = (book: Book) => { 12 | let newDownload: BookDownload = { ...book, loaded: 0, total: 0 }; 13 | downloadStore.update((curr) => ({ 14 | ...curr, 15 | downloads: [newDownload, ...curr.downloads] 16 | })); 17 | }; 18 | 19 | export const updateDownloadStatus = (book: Book, loaded: number, total: number, done = false) => { 20 | downloadStore.update((curr) => ({ 21 | ...curr, 22 | downloads: curr.downloads.map((b) => { 23 | if (book.id === b.id) { 24 | return { 25 | ...b, 26 | loaded, 27 | total, 28 | done 29 | }; 30 | } 31 | return b; 32 | }) 33 | })); 34 | }; 35 | 36 | export default downloadStore; 37 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | span]:text-muted-foreground flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", 16 | className 17 | )} 18 | {...$$restProps} 19 | let:builder 20 | on:click 21 | on:keydown 22 | > 23 | 24 |
    25 | 26 |
    27 |
    28 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 39 |
    40 | 41 |
    42 | 43 | 44 | {#if $globalStore.loading} 45 | 46 | {/if} 47 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-content.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 |
    37 | 38 |
    39 |
    40 | -------------------------------------------------------------------------------- /src/lib/components/ui/select/select-item.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {label || value} 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | -------------------------------------------------------------------------------- /src/lib/components/ui/dialog/dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 28 | 29 | 32 | 33 | Close 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libros", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --check . && eslint .", 12 | "format": "prettier --write ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "^3.0.0", 16 | "@sveltejs/adapter-netlify": "^4.3.4", 17 | "@sveltejs/kit": "^2.0.0", 18 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 19 | "@types/axios": "^0.14.0", 20 | "@types/eslint": "^9.6.0", 21 | "@types/jsdom": "^21.1.7", 22 | "autoprefixer": "^10.4.20", 23 | "eslint": "^9.0.0", 24 | "eslint-config-prettier": "^9.1.0", 25 | "eslint-plugin-svelte": "^2.36.0", 26 | "globals": "^15.0.0", 27 | "prettier": "^3.1.1", 28 | "prettier-plugin-svelte": "^3.1.2", 29 | "prettier-plugin-tailwindcss": "^0.6.5", 30 | "svelte": "^4.2.7", 31 | "svelte-check": "^4.0.0", 32 | "tailwindcss": "^3.4.9", 33 | "typescript": "^5.0.0", 34 | "typescript-eslint": "^8.0.0", 35 | "vite": "^5.0.3" 36 | }, 37 | "type": "module", 38 | "dependencies": { 39 | "@types/node": "^22.7.4", 40 | "axios": "^1.7.7", 41 | "bits-ui": "^0.21.15", 42 | "clsx": "^2.1.1", 43 | "fs": "^0.0.1-security", 44 | "jsdom": "^25.0.1", 45 | "lucide-svelte": "^0.446.0", 46 | "mode-watcher": "^0.4.1", 47 | "node-fetch": "^2.7.0", 48 | "path": "^0.12.7", 49 | "stream": "^0.0.3", 50 | "svelte-sonner": "^0.3.28", 51 | "tailwind-merge": "^2.5.2", 52 | "tailwind-variants": "^0.2.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/components/Paginator.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | {#if currentPage} 25 | preceedToPage(currentPage - 1)} /> 26 | {/if} 27 | 28 | {#each pages as page (page.key)} 29 | {#if page.type === 'ellipsis'} 30 | 31 | 32 | 33 | {:else} 34 | 35 | preceedToPage(page.value)} 37 | {page} 38 | isActive={currentPage == page.value} 39 | > 40 | {page.value} 41 | 42 | 43 | {/if} 44 | {/each} 45 | 46 | {#if currentPage} 47 | preceedToPage(currentPage + 1)} /> 48 | {/if} 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, tv } from "tailwind-variants"; 2 | import type { Button as ButtonPrimitive } from "bits-ui"; 3 | import Root from "./button.svelte"; 4 | 5 | const buttonVariants = tv({ 6 | base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 7 | variants: { 8 | variant: { 9 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 10 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 11 | outline: 12 | "border-input bg-background hover:bg-accent hover:text-accent-foreground border", 13 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 14 | ghost: "hover:bg-accent hover:text-accent-foreground", 15 | link: "text-primary underline-offset-4 hover:underline", 16 | }, 17 | size: { 18 | default: "h-10 px-4 py-2", 19 | sm: "h-9 rounded-md px-3", 20 | lg: "h-11 rounded-md px-8", 21 | icon: "h-10 w-10", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | }); 29 | 30 | type Variant = VariantProps["variant"]; 31 | type Size = VariantProps["size"]; 32 | 33 | type Props = ButtonPrimitive.Props & { 34 | variant?: Variant; 35 | size?: Size; 36 | }; 37 | 38 | type Events = ButtonPrimitive.Events; 39 | 40 | export { 41 | Root, 42 | type Props, 43 | type Events, 44 | // 45 | Root as Button, 46 | type Props as ButtonProps, 47 | type Events as ButtonEvents, 48 | buttonVariants, 49 | }; 50 | -------------------------------------------------------------------------------- /src/lib/components/SearchBar.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
    40 |
    41 | { 43 | filterBy = String(option?.value); 44 | }} 45 | > 46 | 47 | 48 | 49 | 50 | {#each options as option} 51 | {option} 52 | {/each} 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --muted: 210 40% 96.1%; 11 | --muted-foreground: 215.4 16.3% 46.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --border: 214.3 31.8% 91.4%; 20 | --input: 214.3 31.8% 91.4%; 21 | 22 | --primary: 222.2 47.4% 11.2%; 23 | --primary-foreground: 210 40% 98%; 24 | 25 | --secondary: 210 40% 96.1%; 26 | --secondary-foreground: 222.2 47.4% 11.2%; 27 | 28 | --accent: 210 40% 96.1%; 29 | --accent-foreground: 222.2 47.4% 11.2%; 30 | 31 | --destructive: 0 72.2% 50.6%; 32 | --destructive-foreground: 210 40% 98%; 33 | 34 | --ring: 222.2 84% 4.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 222.2 84% 4.9%; 41 | --foreground: 210 40% 98%; 42 | 43 | --muted: 217.2 32.6% 17.5%; 44 | --muted-foreground: 215 20.2% 65.1%; 45 | 46 | --popover: 222.2 84% 4.9%; 47 | --popover-foreground: 210 40% 98%; 48 | 49 | --card: 222.2 84% 4.9%; 50 | --card-foreground: 210 40% 98%; 51 | 52 | --border: 217.2 32.6% 17.5%; 53 | --input: 217.2 32.6% 17.5%; 54 | 55 | --primary: 210 40% 98%; 56 | --primary-foreground: 222.2 47.4% 11.2%; 57 | 58 | --secondary: 217.2 32.6% 17.5%; 59 | --secondary-foreground: 210 40% 98%; 60 | 61 | --accent: 217.2 32.6% 17.5%; 62 | --accent-foreground: 210 40% 98%; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 210 40% 98%; 66 | 67 | --ring: 212.7 26.8% 83.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/lib/components/DownloadProgress.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | {#if $downloadStore.downloads.length < 1} 10 |

    You dont have any downloads so far in this session

    11 | {:else} 12 | Dont close this browser tab until downloads finish 15 | {/if} 16 |
    17 | {#each $downloadStore.downloads as download} 18 |
    19 |

    {download.title.replace(/\d{5,}/g, '')}

    20 |

    {download.authors}

    21 | {#if download.total === 0} 22 | 23 | 24 | Please wait 26 | {:else if download.total === download.loaded} 27 | Finished 28 | {:else} 29 | 30 |

    31 | {Math.round((download.loaded / (1024 * 1024) + Number.EPSILON) * 100) / 100} mb / {Math.round( 32 | (download.total / (1024 * 1024) + Number.EPSILON) * 100 33 | ) / 100} mb 34 |

    35 | {/if} 36 |
    37 | {/each} 38 |
    39 |
    40 | 41 | 59 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { fontFamily } from 'tailwindcss/defaultTheme'; 2 | import type { Config } from 'tailwindcss'; 3 | 4 | const config: Config = { 5 | darkMode: ['class'], 6 | content: ['./src/**/*.{html,js,svelte,ts}'], 7 | safelist: ['dark'], 8 | theme: { 9 | container: { 10 | center: true, 11 | padding: '2rem', 12 | screens: { 13 | '2xl': '1400px' 14 | } 15 | }, 16 | extend: { 17 | colors: { 18 | 'primary-dark': '#2E2532', 19 | 'primary-white': '#FBFBFB', 20 | border: 'hsl(var(--border) / )', 21 | input: 'hsl(var(--input) / )', 22 | ring: 'hsl(var(--ring) / )', 23 | background: 'hsl(var(--background) / )', 24 | foreground: 'hsl(var(--foreground) / )', 25 | primary: { 26 | DEFAULT: 'hsl(var(--primary) / )', 27 | foreground: 'hsl(var(--primary-foreground) / )' 28 | }, 29 | secondary: { 30 | DEFAULT: 'hsl(var(--secondary) / )', 31 | foreground: 'hsl(var(--secondary-foreground) / )' 32 | }, 33 | destructive: { 34 | DEFAULT: 'hsl(var(--destructive) / )', 35 | foreground: 'hsl(var(--destructive-foreground) / )' 36 | }, 37 | muted: { 38 | DEFAULT: 'hsl(var(--muted) / )', 39 | foreground: 'hsl(var(--muted-foreground) / )' 40 | }, 41 | accent: { 42 | DEFAULT: 'hsl(var(--accent) / )', 43 | foreground: 'hsl(var(--accent-foreground) / )' 44 | }, 45 | popover: { 46 | DEFAULT: 'hsl(var(--popover) / )', 47 | foreground: 'hsl(var(--popover-foreground) / )' 48 | }, 49 | card: { 50 | DEFAULT: 'hsl(var(--card) / )', 51 | foreground: 'hsl(var(--card-foreground) / )' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | }, 59 | fontFamily: { 60 | sans: [...fontFamily.sans] 61 | } 62 | } 63 | } 64 | }; 65 | 66 | export default config; 67 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import { cubicOut } from 'svelte/easing'; 4 | import type { TransitionConfig } from 'svelte/transition'; 5 | import type Book from './types/Book'; 6 | import type { BookDownload } from './types/Book'; 7 | import { DOWNLOAD_HISTORY } from './constants'; 8 | 9 | export function cn(...inputs: ClassValue[]) { 10 | return twMerge(clsx(inputs)); 11 | } 12 | 13 | type FlyAndScaleParams = { 14 | y?: number; 15 | x?: number; 16 | start?: number; 17 | duration?: number; 18 | }; 19 | 20 | export const flyAndScale = ( 21 | node: Element, 22 | params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } 23 | ): TransitionConfig => { 24 | const style = getComputedStyle(node); 25 | const transform = style.transform === 'none' ? '' : style.transform; 26 | 27 | const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => { 28 | const [minA, maxA] = scaleA; 29 | const [minB, maxB] = scaleB; 30 | 31 | const percentage = (valueA - minA) / (maxA - minA); 32 | const valueB = percentage * (maxB - minB) + minB; 33 | 34 | return valueB; 35 | }; 36 | 37 | const styleToString = (style: Record): string => { 38 | return Object.keys(style).reduce((str, key) => { 39 | if (style[key] === undefined) return str; 40 | return str + `${key}:${style[key]};`; 41 | }, ''); 42 | }; 43 | 44 | return { 45 | duration: params.duration ?? 200, 46 | delay: 0, 47 | css: (t) => { 48 | const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); 49 | const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); 50 | const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); 51 | 52 | return styleToString({ 53 | transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, 54 | opacity: t 55 | }); 56 | }, 57 | easing: cubicOut 58 | }; 59 | }; 60 | 61 | export const downloadBlob = (blob: Blob, downloadedBook: BookDownload) => { 62 | const { title, extension } = downloadedBook; 63 | const url = window.URL.createObjectURL(blob); 64 | const a = document.createElement('a'); 65 | a.style.display = 'none'; 66 | a.href = url; 67 | a.download = `${title}.${extension}`; // Specify the filename 68 | document.body.appendChild(a); 69 | a.click(); 70 | window.URL.revokeObjectURL(url); // Clean up 71 | 72 | let prevStorage = localStorage.getItem(DOWNLOAD_HISTORY); 73 | let downloads: Book[] = []; 74 | 75 | if (prevStorage !== '' && prevStorage !== null) { 76 | downloads = JSON.parse(prevStorage); 77 | } 78 | 79 | downloads.push(downloadedBook); 80 | localStorage.setItem(DOWNLOAD_HISTORY, JSON.stringify(downloads)); 81 | }; 82 | -------------------------------------------------------------------------------- /src/routes/search/+page.svelte: -------------------------------------------------------------------------------- 1 | 61 | 62 |
    63 |
    64 |
    65 | 70 | 71 |
    72 | 73 |

    74 | Search results for 75 | {query} 76 |

    77 |
    78 | (searchResults = [])} /> 79 | {#if searchResults.length == 0 && !$globalStore.loading} 80 |
    81 |

    Looks like we got no books for this search

    82 |
    83 | {/if} 84 |
    85 | {#each searchResults as book, i (i)} 86 | 87 | {/each} 88 | 89 | {#if $globalStore.loading && searchResults.length < 1} 90 | 91 | 92 | 93 | 94 | 95 | 96 | {/if} 97 |
    98 |
    99 | {#if totalPages && searchResults} 100 | (searchResults = [])} 107 | /> 108 | {/if} 109 |
    110 |
    111 | 112 | 113 | {}}> 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/lib/components/BookUI.svelte: -------------------------------------------------------------------------------- 1 | 83 | 84 |
    87 |
    88 | 93 |
    94 |
    95 |
    96 |

    97 | {book.title.replace(/\d{5,}/g, '').slice(0, 100)} 98 | {book.title.replace(/\d{5,}/g, '').length > 100 ? '....' : ''} 99 |

    100 |
    101 | 102 |
    103 |

    104 | Authors : 105 | 106 | {#if book.authors.length} 107 | {book.authors.length > 100 109 | ? `${book.authors.slice(0, 100)} ....` 110 | : book.authors} 112 | {:else} 113 | {book.authors.length && NOT_AVAILABLE} 114 | {/if} 115 |

    116 |

    117 | {book.year || NOT_AVAILABLE} 118 |

    119 |
    120 |
    121 |
    122 | 123 | handleDownload(book)} 124 | > 127 | 128 | 129 | 130 | 131 |
    132 |
    133 | --------------------------------------------------------------------------------