diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index dfdd0e3..48cfaa8 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -28,7 +28,7 @@ jobs: TEMPLATE_NAME=$(basename "$dir") COMPOSE_FILE="$dir/docker-compose.yml" - TEMPLATE_FILE="$dir/template.yml" + TEMPLATE_FILE="$dir/template.toml" if [ ! -f "$COMPOSE_FILE" ]; then echo "โŒ Missing docker-compose.yml in $TEMPLATE_NAME" @@ -36,7 +36,7 @@ jobs: fi if [ ! -f "$TEMPLATE_FILE" ]; then - echo "โŒ Missing template.yml in $TEMPLATE_NAME" + echo "โŒ Missing template.toml in $TEMPLATE_NAME" ERROR=1 fi fi @@ -49,6 +49,82 @@ jobs: echo "โœ… Blueprint folders validated successfully." fi + - name: Validate meta.json structure and required fields + run: | + echo "๐Ÿ” Validating meta.json structure and required fields..." + + # First check if meta.json exists and is valid JSON + if [ ! -f "meta.json" ]; then + echo "โŒ meta.json file not found" + exit 1 + fi + + if ! jq empty meta.json 2>/dev/null; then + echo "โŒ meta.json is not a valid JSON file" + exit 1 + fi + + ERROR=0 + ERRORS_FOUND="" + + # Debug: Show total number of entries + TOTAL_ENTRIES=$(jq '. | length' meta.json) + echo "๐Ÿ“Š Total entries in meta.json: $TOTAL_ENTRIES" + + # Get all entries at once and process them + TOTAL_INDEX=$(($TOTAL_ENTRIES - 1)) + + for i in $(seq 0 $TOTAL_INDEX); do + entry=$(jq -c ".[$i]" meta.json) + INDEX=$((i + 1)) + + echo "-------------------------------------------" + echo "๐Ÿ” Checking entry #$INDEX..." + + # Get the ID for better error reporting + ID=$(echo "$entry" | jq -r '.id // "UNKNOWN"') + echo "๐Ÿ“ Processing entry with ID: $ID" + + # Validate required top-level fields + for field in "id" "name" "version" "description" "logo" "links" "tags"; do + if [ "$(echo "$entry" | jq "has(\"$field\")")" != "true" ]; then + ERROR_MSG="โŒ Entry #$INDEX (ID: $ID) is missing required field: $field" + echo "$ERROR_MSG" + ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n" + ERROR=1 + fi + done + + # Validate links object required fields + if [ "$(echo "$entry" | jq 'has("links")')" == "true" ]; then + for link_field in "github" "website" "docs"; do + if [ "$(echo "$entry" | jq ".links | has(\"$link_field\")")" != "true" ]; then + ERROR_MSG="โŒ Entry #$INDEX (ID: $ID): links object is missing required field: $link_field" + echo "$ERROR_MSG" + ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n" + ERROR=1 + fi + done + fi + + # Validate tags array is not empty + if [ "$(echo "$entry" | jq '.tags | length')" -eq 0 ]; then + ERROR_MSG="โŒ Entry #$INDEX (ID: $ID): tags array cannot be empty" + echo "$ERROR_MSG" + ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n" + ERROR=1 + fi + done + + echo "-------------------------------------------" + if [ $ERROR -eq 1 ]; then + echo "โŒ meta.json structure validation failed." + echo -e "Summary of all errors found:${ERRORS_FOUND}" + exit 1 + else + echo "โœ… meta.json structure validated successfully." + fi + - name: Validate meta.json matches blueprint folders and logo files run: | echo "๐Ÿ” Validating meta.json against blueprint folders and logos..." diff --git a/README.md b/README.md index 10ecec7..fcb2886 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This is the official repository for the Dokploy Open Source Templates. 1. Fork the repository 2. Create a new branch -3. Add the template to the `blueprints` folder (docker-compose.yml, template.yml) +3. Add the template to the `blueprints` folder (`docker-compose.yml`, `template.toml`) 4. Add the template metadata (name, description, version, logo, links, tags) to the `meta.json` file 5. Add the logo to the template folder 6. Commit and push your changes @@ -45,19 +45,22 @@ services: volumes: grafana-storage: {} ``` -3. Add the `template.yml` file to the folder, this is where we specify the domains, mounts and env variables, to understand more the structure of `template.yml` you can read here [Template.yml structure](#templateyml-structure) +3. Add the `template.toml` file to the folder, this is where we specify the domains, mounts and env variables, to understand more the structure of `template.toml` you can read here [Template.toml structure](#template.toml-structure) -```yaml -variables: - main_domain: ${domain} +```toml +[variables] +main_domain = "${domain}" -config: - domains: - - serviceName: grafana - port: 3000 - host: ${main_domain} - env: [] - mounts: [] +[config] +[[config.domains]] +serviceName = "grafana" +port = 3000 +host = "${main_domain}" + + +[config.env] + +[[config.mounts]] ``` 4. Add meta information to the `meta.json` file in the root folder @@ -82,51 +85,53 @@ config: 6. Commit and push your changes 7. Create a pull request -### Template.yml structure - -Dokploy use a defined structure for the `template.yml` file, we have 4 sections available: +### Template.toml structure +Dokploy use a defined structure for the `template.toml` file, we have 4 sections available: 1. `variables`: This is where we define the variables that will be used in the `domains`, `env` and `mounts` sections. 2. `domains`: This is where we define the configuration for the template. 3. `env`: This is where we define the environment variables for the template. 4. `mounts`: This is where we define the mounts for the template. - - The `variables(Optional)` structure is the following: -```yaml -variables: - main_domain: ${domain} - my_domain: https://my-domain.com - my_password: ${password:32} - any_helper: ${you-can-use-any-helper} +```toml +[variables] +main_domain = "${domain}" +my_domain = "https://my-domain.com" +my_password = "${password:32}" +any_helper = "${you-can-use-any-helper}" ``` - The `config` structure is the following: -```yaml -config: - domains: # Optional - - serviceName: grafana # Required - port: 3000 # Required - host: ${main_domain} # Required - path: / # Optional +```toml +[config] +# Optional sections below - env: # Optional - - AP_HOST=${main_domain} - - AP_API_KEY=${api_key} - - AP_ENCRYPTION_KEY=${encryption_key} - - AP_JWT_SECRET=${jwt_secret} - - AP_POSTGRES_PASSWORD=${postgres_password} +[[config.domains]] +serviceName = "grafana" # Required +port = 3000 # Required +host = "${main_domain}" # Required +path = "/" # Optional - mounts: # Optional or [] - - filePath: /content/file.txt - content: | - My content +env = [ + "AP_HOST=${main_domain}", + "AP_API_KEY=${api_key}", + "AP_ENCRYPTION_KEY=${encryption_key}", + "AP_JWT_SECRET=${jwt_secret}", + "AP_POSTGRES_PASSWORD=${postgres_password}" +] + +[[config.mounts]] +filePath = "/content/file.txt" +content = """ +My content +""" ``` -Important: you can reference any variable in the `domains`, `env` and `mounts` sections. just use the `${variable_name}` syntax, in the case you don't want to define a variable, you can use the `domain`, `base64`, `password`, `hash`, `uuid`, `randomPort` or `timestamp` helpers. +Important: you can reference any variable in the `domains`, `env` and `mounts` sections. just use the `${variable_name}` syntax, in the case you don't want to define a variable, you can use the `domain`, `base64`, `password`, `hash`, `uuid`, `randomPort`, `timestamp`, `jwt`, `email`, or `username` helpers. ### Helpers @@ -140,6 +145,8 @@ We have a few helpers that are very common when creating a template, these are: - `randomPort`: This is a helper that will generate a random port for the template. - `timestamp`: This is a helper that will generate a timestamp. - `jwt or jwt:length`: This is a helper that will generate a jwt for the template. +- `email`: This is a helper that will generate a random email for the template. +- `username`: This is a helper that will generate a random username in lowercase for the template. diff --git a/app/package.json b/app/package.json index a0f490f..49503ad 100644 --- a/app/package.json +++ b/app/package.json @@ -16,6 +16,7 @@ "@codemirror/language": "^6.10.1", "@codemirror/legacy-modes": "6.4.0", "@codemirror/view": "6.29.0", + "@iarna/toml": "^2.2.5", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", @@ -33,11 +34,13 @@ "next-themes": "^0.4.5", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "^7.4.1", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", "tailwindcss": "^4.0.12", "tailwindcss-animate": "^1.0.7", "vite-plugin-static-copy": "2.3.0", + "yaml": "2.7.1", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 3669f77..9fec5d4 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@codemirror/view': specifier: 6.29.0 version: 6.29.0 + '@iarna/toml': + specifier: ^2.2.5 + version: 2.2.5 '@radix-ui/react-dialog': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -46,7 +49,7 @@ importers: version: 1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tailwindcss/vite': specifier: ^4.0.12 - version: 4.0.12(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 4.0.12(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1)) '@uiw/codemirror-theme-github': specifier: ^4.22.1 version: 4.23.10(@codemirror/language@6.10.8)(@codemirror/state@6.5.2)(@codemirror/view@6.29.0) @@ -77,6 +80,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-router-dom: + specifier: ^7.4.1 + version: 7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) sonner: specifier: ^2.0.1 version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -91,7 +97,10 @@ importers: version: 1.0.7(tailwindcss@4.0.12) vite-plugin-static-copy: specifier: 2.3.0 - version: 2.3.0(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 2.3.0(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1)) + yaml: + specifier: 2.7.1 + version: 2.7.1 zustand: specifier: ^5.0.3 version: 5.0.3(@types/react@19.0.10)(react@19.0.0) @@ -107,7 +116,7 @@ importers: version: 19.0.4(@types/react@19.0.10) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)) + version: 4.3.4(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1)) globals: specifier: ^15.15.0 version: 15.15.0 @@ -116,7 +125,7 @@ importers: version: 5.7.3 vite: specifier: ^6.2.0 - version: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2) + version: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1) packages: @@ -408,6 +417,9 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@iarna/toml@2.2.5': + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1112,6 +1124,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1211,6 +1226,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -1489,6 +1508,23 @@ packages: '@types/react': optional: true + react-router-dom@7.4.1: + resolution: {integrity: sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.4.1: + resolution: {integrity: sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -1529,6 +1565,9 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + sonner@2.0.1: resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==} peerDependencies: @@ -1567,6 +1606,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -1657,6 +1699,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.7.1: + resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} + engines: {node: '>= 14'} + hasBin: true + zustand@5.0.3: resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} engines: {node: '>=12.20.0'} @@ -1965,6 +2012,8 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@iarna/toml@2.2.5': {} + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -2581,13 +2630,13 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.0.12 '@tailwindcss/oxide-win32-x64-msvc': 4.0.12 - '@tailwindcss/vite@4.0.12(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2))': + '@tailwindcss/vite@4.0.12(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1))': dependencies: '@tailwindcss/node': 4.0.12 '@tailwindcss/oxide': 4.0.12 lightningcss: 1.29.2 tailwindcss: 4.0.12 - vite: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1) '@types/babel__core@7.20.5': dependencies: @@ -2610,6 +2659,8 @@ snapshots: dependencies: '@babel/types': 7.26.9 + '@types/cookie@0.6.0': {} + '@types/estree@1.0.6': {} '@types/node@20.17.24': @@ -2665,14 +2716,14 @@ snapshots: - '@codemirror/lint' - '@codemirror/search' - '@vitejs/plugin-react@4.3.4(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2))': + '@vitejs/plugin-react@4.3.4(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1))': dependencies: '@babel/core': 7.26.9 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.9) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1) transitivePeerDependencies: - supports-color @@ -2740,6 +2791,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.0.2: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -2982,6 +3035,22 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + react-router-dom@7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-router: 7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + + react-router@7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@types/cookie': 0.6.0 + cookie: 1.0.2 + react: 19.0.0 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 19.0.0(react@19.0.0) + react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0): dependencies: get-nonce: 1.0.1 @@ -3033,6 +3102,8 @@ snapshots: semver@6.3.1: {} + set-cookie-parser@2.7.1: {} + sonner@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: react: 19.0.0 @@ -3060,6 +3131,8 @@ snapshots: tslib@2.8.1: {} + turbo-stream@2.4.0: {} + typescript@5.7.3: {} undici-types@6.19.8: {} @@ -3087,16 +3160,16 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 - vite-plugin-static-copy@2.3.0(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)): + vite-plugin-static-copy@2.3.0(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1)): dependencies: chokidar: 3.6.0 fast-glob: 3.3.3 fs-extra: 11.3.0 p-map: 7.0.3 picocolors: 1.1.1 - vite: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1) - vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2): + vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.7.1): dependencies: esbuild: 0.25.1 postcss: 8.5.3 @@ -3106,11 +3179,14 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.29.2 + yaml: 2.7.1 w3c-keyname@2.2.8: {} yallist@3.1.1: {} + yaml@2.7.1: {} + zustand@5.0.3(@types/react@19.0.10)(react@19.0.0): optionalDependencies: '@types/react': 19.0.10 diff --git a/app/script.js b/app/script.js new file mode 100644 index 0000000..fd64000 --- /dev/null +++ b/app/script.js @@ -0,0 +1,36 @@ +import yaml from "yaml"; +import toml from "@iarna/toml"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function convertYamlToToml(yamlContent) { + const parsedYaml = yaml.parse(yamlContent); + return toml.stringify(parsedYaml); +} + +function processDirectory(dirPath) { + const files = fs.readdirSync(dirPath); + + files.forEach((file) => { + const filePath = path.join(dirPath, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + processDirectory(filePath); + } else if (file === "template.yml") { + console.log(`Converting ${filePath}`); + const yamlContent = fs.readFileSync(filePath, "utf8"); + const tomlContent = convertYamlToToml(yamlContent); + const tomlPath = path.join(dirPath, "template.toml"); + fs.writeFileSync(tomlPath, tomlContent); + } + }); +} + +// Ruta al directorio blueprints relativa al script +const blueprintsPath = path.join(__dirname, "..", "blueprints"); +processDirectory(blueprintsPath); diff --git a/app/src/App.tsx b/app/src/App.tsx index f6c69ec..9864c07 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -3,16 +3,19 @@ import Navigation from "./components/Navigation"; import Search from "./components/Search"; import { useStore } from "@/store"; import "./App.css"; +import { BrowserRouter } from "react-router-dom"; function App() { const view = useStore((state) => state.view); return ( -
- - - -
+ +
+ + + +
+
); } diff --git a/app/src/components/Search.tsx b/app/src/components/Search.tsx index a114c19..a810923 100644 --- a/app/src/components/Search.tsx +++ b/app/src/components/Search.tsx @@ -16,9 +16,10 @@ import { Check, ChevronsUpDown } from "lucide-react"; import React from "react"; import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; import SelectedTags from "./SelectedTags"; +import { useSearchParams } from "react-router-dom"; const Search = () => { - const { templates, searchQuery, setSearchQuery, setView, templatesCount } = + const { templates, searchQuery, setSearchQuery, setView, templatesCount, setFilteredTemplates, setTemplatesCount } = useStore(); const selectedTags = useStore((state) => state.selectedTags); const addSelectedTag = useStore((state) => state.addSelectedTag); @@ -26,6 +27,7 @@ const Search = () => { const [open, setOpen] = React.useState(false); const [tagSearch, setTagSearch] = React.useState(""); const view = useStore((state) => state.view); + const [searchParams, setSearchParams] = useSearchParams(); // Get all unique tags, safely handle empty templates const uniqueTags = React.useMemo(() => { @@ -43,6 +45,53 @@ const Search = () => { ); }, [uniqueTags, tagSearch]); + // Initialize search query from URL params and apply filters + React.useEffect(() => { + const queryFromUrl = searchParams.get("q") || ""; + if (queryFromUrl !== searchQuery) { + setSearchQuery(queryFromUrl); + } + + // Apply filters whenever templates, search query or selected tags change + if (templates) { + const filtered = templates.filter((template) => { + // Filter by search query + const matchesSearch = template.name + .toLowerCase() + .includes(queryFromUrl.toLowerCase()); + + // Filter by selected tags + const matchesTags = + selectedTags.length === 0 || + selectedTags.every((tag) => template.tags.includes(tag)); + + return matchesSearch && matchesTags; + }); + + setFilteredTemplates(filtered); + setTemplatesCount(filtered.length); + } + }, [searchParams, templates, selectedTags, setSearchQuery, setFilteredTemplates, setTemplatesCount]); + + // Update URL params when search query changes + const handleSearchChange = (e: React.ChangeEvent) => { + const newQuery = e.target.value; + setSearchQuery(newQuery); + if (newQuery) { + setSearchParams({ q: newQuery }); + } else { + searchParams.delete("q"); + setSearchParams(searchParams); + } + }; + + // Clear search and URL params + const handleClearSearch = () => { + setSearchQuery(""); + searchParams.delete("q"); + setSearchParams(searchParams); + }; + return (
{/*

@@ -63,11 +112,11 @@ const Search = () => { type="text" placeholder="Search templates..." value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={handleSearchChange} className="w-full p-6" /> {searchQuery.length > 0 ? ( -
setSearchQuery("")}> +
) : ( diff --git a/app/src/components/TemplateDialog.tsx b/app/src/components/TemplateDialog.tsx index af7fd03..89e0893 100644 --- a/app/src/components/TemplateDialog.tsx +++ b/app/src/components/TemplateDialog.tsx @@ -228,7 +228,7 @@ const TemplateDialog: React.FC = ({