From 414538daf6935e88f7396331ef7f48f72070197c Mon Sep 17 00:00:00 2001 From: Shahrad Elahi Date: Thu, 2 Nov 2023 16:32:33 +0330 Subject: [PATCH] add `tailwindcss` and `shadcn` --- .dockerignore | 2 +- Dockerfile | 84 ++++++++++-------- Dockerfile-Dev | 54 ++++++----- docker-compose.dev.yml | 4 +- package.json | 2 +- pnpm-lock.yaml | 5 ++ web/.prettierrc | 3 +- web/bun.lockb | Bin 52176 -> 92848 bytes web/components.json | 13 +++ web/package.json | 22 +++-- web/postcss.config.cjs | 13 +++ web/src/app.postcss | 78 ++++++++++++++++ web/src/index.test.ts | 7 ++ .../lib/components/ui/button/button.svelte | 24 +++++ web/src/lib/components/ui/button/index.ts | 51 +++++++++++ web/src/lib/utils.ts | 62 +++++++++++++ web/src/routes/+layout.svelte | 1 + web/src/routes/+page.svelte | 6 ++ web/svelte.config.js | 21 +++-- web/tailwind.config.js | 64 +++++++++++++ web/vite.config.ts | 7 +- 21 files changed, 435 insertions(+), 88 deletions(-) create mode 100644 pnpm-lock.yaml create mode 100644 web/components.json create mode 100644 web/postcss.config.cjs create mode 100644 web/src/app.postcss create mode 100644 web/src/index.test.ts create mode 100644 web/src/lib/components/ui/button/button.svelte create mode 100644 web/src/lib/components/ui/button/index.ts create mode 100644 web/src/lib/utils.ts create mode 100644 web/src/routes/+layout.svelte create mode 100644 web/tailwind.config.js diff --git a/.dockerignore b/.dockerignore index 54a76d2..d470bcd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ .idea -node_modules +.ignore assets diff --git a/Dockerfile b/Dockerfile index b17f89e..9654932 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ -FROM node:alpine as base -WORKDIR /app +FROM oven/bun:alpine as base +LABEL Maintainer="Shahrad Elahi " +WORKDIR /usr/src/app ENV TZ=UTC RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone @@ -7,51 +8,58 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY --from=chriswayg/tor-alpine:latest /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy COPY --from=chriswayg/tor-alpine:latest /usr/local/bin/meek-server /usr/local/bin/meek-server +# Set the mirror list +RUN echo "https://uk.alpinelinux.org/alpine/latest-stable/main" > /etc/apk/repositories && \ + echo "https://mirror.bardia.tech/alpine/latest-stable/main" >> /etc/apk/repositories && \ + echo "https://uk.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ + echo "https://mirror.bardia.tech/alpine/latest-stable/community" >> /etc/apk/repositories + +# Update and upgrade packages +RUN apk update && apk upgrade + +# Install required packages RUN apk add -U --no-cache \ - iproute2 iptables net-tools \ - screen vim curl bash \ - wireguard-tools \ - openssl \ - dumb-init \ - tor \ - redis + iproute2 iptables net-tools \ + screen vim curl bash \ + wireguard-tools \ + openssl \ + dumb-init \ + tor \ + redis + +# Clear cache +RUN rm -rf /var/cache/apk/* -FROM node:alpine as builder -WORKDIR /app +FROM base AS deps -COPY /src/package.json /src/package-lock.json ./ -RUN npm install +RUN mkdir -p /temp/dev +COPY web/package.json web/bun.lockb /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile +RUN mkdir -p /temp/prod +COPY web/package.json web/bun.lockb /temp/prod/ +RUN cd /temp/prod && bun install --frozen-lockfile --production + + +FROM base AS build +COPY --from=deps /temp/dev/node_modules node_modules +COPY web . + +# build ENV NODE_ENV=production -COPY /src/ . - -RUN npm run build +RUN bun run build -FROM base -WORKDIR /app +FROM base AS release + +COPY --from=deps /temp/prod/node_modules node_modules +COPY --from=build /usr/src/app/build . +COPY --from=build /usr/src/app/package.json . ENV NODE_ENV=production -LABEL Maintainer="Shahrad Elahi " - -COPY /config/torrc /etc/tor/torrc - -COPY --from=builder /app/.next ./.next -COPY --from=builder /app/next.config.js ./next.config.js -COPY --from=builder /app/public ./public - -COPY /src/package.json /src/package-lock.json ./ -RUN npm install --omit dev - +# run the app +USER bun EXPOSE 3000/tcp - -HEALTHCHECK --interval=60s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://127.0.0.1:3000/api/healthcheck || exit 1 - -COPY docker-entrypoint.sh /usr/bin/entrypoint -RUN chmod +x /usr/bin/entrypoint -ENTRYPOINT ["/usr/bin/entrypoint"] - -CMD ["npm", "run", "start"] +CMD [ "bun", "start" ] diff --git a/Dockerfile-Dev b/Dockerfile-Dev index 90d4daf..52a54f8 100644 --- a/Dockerfile-Dev +++ b/Dockerfile-Dev @@ -8,45 +8,41 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY --from=chriswayg/tor-alpine:latest /usr/local/bin/obfs4proxy /usr/local/bin/obfs4proxy COPY --from=chriswayg/tor-alpine:latest /usr/local/bin/meek-server /usr/local/bin/meek-server +# Set the mirror list +RUN echo "https://uk.alpinelinux.org/alpine/latest-stable/main" > /etc/apk/repositories && \ + echo "https://mirror.bardia.tech/alpine/latest-stable/main" >> /etc/apk/repositories && \ + echo "https://uk.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ + echo "https://mirror.bardia.tech/alpine/latest-stable/community" >> /etc/apk/repositories + +# Update and upgrade packages +RUN apk update && apk upgrade + +# Install required packages RUN apk add -U --no-cache \ - iproute2 iptables net-tools \ - screen vim curl bash \ - wireguard-tools \ - openssl \ - dumb-init \ - tor \ - redis + iproute2 iptables net-tools \ + screen vim curl bash \ + wireguard-tools \ + openssl \ + dumb-init \ + tor \ + redis + +# Clear cache +RUN rm -rf /var/cache/apk/* FROM base AS deps RUN mkdir -p /temp/dev -COPY web/package.json web/bun.lockb /temp/dev/ +COPY web/package.json web/bun.lockb web/node_modules* /temp/dev/ RUN cd /temp/dev && bun install --frozen-lockfile -RUN mkdir -p /temp/prod -COPY web/package.json web/bun.lockb /temp/prod/ -RUN cd /temp/prod && bun install --frozen-lockfile --production +FROM base AS runner -FROM install AS build COPY --from=deps /temp/dev/node_modules node_modules COPY web . -# build -ENV NODE_ENV=production -RUN bun run build - - -FROM base AS release - -COPY --from=deps /temp/prod/node_modules node_modules -COPY --from=build /usr/src/app/build . -COPY --from=build /usr/src/app/package.json . - -ENV NODE_ENV=production - -# run the app -USER bun -EXPOSE 3000/tcp -CMD [ "bun", "start" ] \ No newline at end of file +# run the appc +EXPOSE 5173/tcp +CMD [ "bun", "dev", "--host" ] \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3df93d3..d9842a2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -3,7 +3,9 @@ services: wireadmin: image: wireadmin volumes: - - ./src/:/app/ + - ./web/:/usr/src/app/ + ports: + - "5173:5173" environment: - UI_PASSWORD=password - WG_HOST=192.168.1.102 diff --git a/package.json b/package.json index 2edc0fb..eee8d4a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "scripts": { "dev:image": "DOCKER_BUILDKIT=1 docker build --tag wireadmin -f Dockerfile-Dev .", - "dev": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up; docker rm -f wireadmin" + "dev": "docker compose rm -fsv && docker compose -f docker-compose.yml -f docker-compose.dev.yml up && docker compose rm -fsv" }, "keywords": [], "author": "Shahrad Elahi ", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..2b9f188 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/web/.prettierrc b/web/.prettierrc index e3ef508..61b4a62 100644 --- a/web/.prettierrc +++ b/web/.prettierrc @@ -1,5 +1,5 @@ { - "useTabs": false, + "useTabs": true, "singleQuote": true, "trailingComma": "none", "printWidth": 100, @@ -9,7 +9,6 @@ "pluginSearchDirs": [ "." ], - "tabWidth": 2, "overrides": [ { "files": "*.svelte", diff --git a/web/bun.lockb b/web/bun.lockb index cc5e752eca45142e10a44fbcf7350de49eda9841..05028ed816a1619c50867b0d91c959aa75337f3d 100644 GIT binary patch delta 34622 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 WUH|#vbNZcMo-WdDf-t?%`0{_2#?Sr$ delta 9930 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 { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/web/src/lib/components/ui/button/button.svelte b/web/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..c08852d --- /dev/null +++ b/web/src/lib/components/ui/button/button.svelte @@ -0,0 +1,24 @@ + + + + + diff --git a/web/src/lib/components/ui/button/index.ts b/web/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..8363f96 --- /dev/null +++ b/web/src/lib/components/ui/button/index.ts @@ -0,0 +1,51 @@ +import Root from './button.svelte'; +import { tv, type VariantProps } from 'tailwind-variants'; +import type { Button as ButtonPrimitive } from 'bits-ui'; + +const buttonVariants = tv({ + base: 'inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline' + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } +}); + +type Variant = VariantProps['variant']; +type Size = VariantProps['size']; + +type Props = ButtonPrimitive.Props & { + variant?: Variant; + size?: Size; +}; + +type Events = ButtonPrimitive.Events; + +export { + Root, + type Props, + type Events, + // + Root as Button, + type Props as ButtonProps, + type Events as ButtonEvents, + buttonVariants +}; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000..9829373 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,62 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; +import { cubicOut } from 'svelte/easing'; +import type { TransitionConfig } from 'svelte/transition'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +type FlyAndScaleParams = { + y?: number; + x?: number; + start?: number; + duration?: number; +}; + +export const flyAndScale = ( + node: Element, + params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } +): TransitionConfig => { + const style = getComputedStyle(node); + const transform = style.transform === 'none' ? '' : style.transform; + + const scaleConversion = ( + valueA: number, + scaleA: [ number, number ], + scaleB: [ number, number ] + ) => { + const [ minA, maxA ] = scaleA; + const [ minB, maxB ] = scaleB; + + const percentage = (valueA - minA) / (maxA - minA); + const valueB = percentage * (maxB - minB) + minB; + + return valueB; + }; + + const styleToString = ( + style: Record + ): string => { + return Object.keys(style).reduce((str, key) => { + if (style[key] === undefined) return str; + return str + `${key}:${style[key]};`; + }, ''); + }; + + return { + duration: params.duration ?? 200, + delay: 0, + css: (t) => { + const y = scaleConversion(t, [ 0, 1 ], [ params.y ?? 5, 0 ]); + const x = scaleConversion(t, [ 0, 1 ], [ params.x ?? 0, 0 ]); + const scale = scaleConversion(t, [ 0, 1 ], [ params.start ?? 0.95, 1 ]); + + return styleToString({ + transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, + opacity: t + }); + }, + easing: cubicOut + }; +}; \ No newline at end of file diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte new file mode 100644 index 0000000..bfbbe64 --- /dev/null +++ b/web/src/routes/+layout.svelte @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 5982b0a..954fd17 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,2 +1,8 @@ + +

Welcome to SvelteKit

Visit kit.svelte.dev to read the documentation

+ + \ No newline at end of file diff --git a/web/svelte.config.js b/web/svelte.config.js index ecaf275..577359f 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -3,16 +3,19 @@ import { vitePreprocess } from '@sveltejs/kit/vite'; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: [ vitePreprocess({}) ], - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter() - } + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter(), + alias: { + $lib: './src/lib' + } + } }; export default config; diff --git a/web/tailwind.config.js b/web/tailwind.config.js new file mode 100644 index 0000000..f15fd8d --- /dev/null +++ b/web/tailwind.config.js @@ -0,0 +1,64 @@ +import { fontFamily } from 'tailwindcss/defaultTheme'; + +/** @type {import('tailwindcss').Config} */ +const config = { + darkMode: [ 'class' ], + content: [ './src/**/*.{html,js,svelte,ts}' ], + safelist: [ 'dark' ], + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border) / )', + input: 'hsl(var(--input) / )', + ring: 'hsl(var(--ring) / )', + background: 'hsl(var(--background) / )', + foreground: 'hsl(var(--foreground) / )', + primary: { + DEFAULT: 'hsl(var(--primary) / )', + foreground: 'hsl(var(--primary-foreground) / )' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary) / )', + foreground: 'hsl(var(--secondary-foreground) / )' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive) / )', + foreground: 'hsl(var(--destructive-foreground) / )' + }, + muted: { + DEFAULT: 'hsl(var(--muted) / )', + foreground: 'hsl(var(--muted-foreground) / )' + }, + accent: { + DEFAULT: 'hsl(var(--accent) / )', + foreground: 'hsl(var(--accent-foreground) / )' + }, + popover: { + DEFAULT: 'hsl(var(--popover) / )', + foreground: 'hsl(var(--popover-foreground) / )' + }, + card: { + DEFAULT: 'hsl(var(--card) / )', + foreground: 'hsl(var(--card-foreground) / )' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + fontFamily: { + sans: [ ...fontFamily.sans ] + } + } + } +}; + +export default config; diff --git a/web/vite.config.ts b/web/vite.config.ts index 0842f4d..d9844e7 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,6 +1,9 @@ import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ - plugins: [ sveltekit() ] + plugins: [ sveltekit() ], + test: { + include: [ 'src/**/*.{test,spec}.{js,ts}' ] + } });