diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index 1f49d818f..ddc11cadd 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -18,13 +18,13 @@ jobs: name: Install pnpm id: pnpm-install with: - version: 9.1.0 + version: 9.8.0 run_install: false - name: Use node_modules cache uses: actions/cache@v4 id: nm-cache with: - path: 'svelte-app/node_modules' + path: "svelte-app/node_modules" key: ${{ runner.os }}-npm-${{ hashFiles('svelte-app/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-nm-16- diff --git a/.github/workflows/sanity.yml b/.github/workflows/sanity.yml index 6f0f16f7d..8ad5d7d7b 100644 --- a/.github/workflows/sanity.yml +++ b/.github/workflows/sanity.yml @@ -2,16 +2,16 @@ name: CI - Sanity on: push: - branches: [ "main" ] + branches: ["main"] paths-ignore: - - '**/*.md' - - 'LICENSE' - - '**/.husky' - - 'svelte-app/**' - - 'express-api/**' - - 'elixir-api/**' + - "**/*.md" + - "LICENSE" + - "**/.husky" + - "svelte-app/**" + - "express-api/**" + - "elixir-api/**" -concurrency: +concurrency: group: sanity cancel-in-progress: true @@ -30,13 +30,13 @@ jobs: name: Install pnpm id: pnpm-install with: - version: 9.1.0 + version: 9.8.0 run_install: false - name: Use node_modules cache uses: actions/cache@v4 id: nm-cache with: - path: 'sanity-cms/node_modules' + path: "sanity-cms/node_modules" key: ${{ runner.os }}-npm-${{ hashFiles('sanity-cms/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-npm- diff --git a/.github/workflows/svelte.yml b/.github/workflows/svelte.yml index b12ad5d8f..84419f1be 100644 --- a/.github/workflows/svelte.yml +++ b/.github/workflows/svelte.yml @@ -2,17 +2,17 @@ name: CI on: push: - branches: [ "main" ] + branches: ["main"] paths-ignore: - - '**/*.md' - - 'LICENSE' - - '**/.husky' - - 'sanity-cms/**' - - 'express-api/**' - - 'rust-api/**' - - 'elixir-api/**' + - "**/*.md" + - "LICENSE" + - "**/.husky" + - "sanity-cms/**" + - "express-api/**" + - "rust-api/**" + - "elixir-api/**" -concurrency: +concurrency: group: ci cancel-in-progress: true @@ -31,13 +31,13 @@ jobs: name: Install pnpm id: pnpm-install with: - version: 9.1.0 + version: 9.8.0 run_install: false - name: Use node_modules cache uses: actions/cache@v4 id: nm-cache with: - path: 'svelte-app/node_modules' + path: "svelte-app/node_modules" key: ${{ runner.os }}-npm-${{ hashFiles('svelte-app/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-npm- @@ -52,6 +52,6 @@ jobs: secrets: inherit deploy: name: Deploy - needs: [ lint, tests ] + needs: [lint, tests] uses: ./.github/workflows/netlify-deploy.yml secrets: inherit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1bd1478bd..152e7a7c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,10 +2,10 @@ name: Tests on: pull_request: - branches: [ "main" ] + branches: ["main"] workflow_call: -concurrency: +concurrency: group: test-${{ github.ref }} cancel-in-progress: true @@ -23,13 +23,13 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 9.1.0 + version: 9.8.0 run_install: false - name: Use node_modules cache uses: actions/cache@v4 id: nm-cache with: - path: 'svelte-app/node_modules' + path: "svelte-app/node_modules" key: ${{ runner.os }}-npm-${{ hashFiles('svelte-app/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-npm- @@ -52,13 +52,13 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 9.1.0 + version: 9.8.0 run_install: false - name: Use node_modules cache uses: actions/cache@v4 id: nm-cache with: - path: 'svelte-app/node_modules' + path: "svelte-app/node_modules" key: ${{ runner.os }}-npm-${{ hashFiles('svelte-app/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-npm- diff --git a/elixir-api/lib/app/application.ex b/elixir-api/lib/app/application.ex index dcc10ab41..602313774 100644 --- a/elixir-api/lib/app/application.ex +++ b/elixir-api/lib/app/application.ex @@ -30,7 +30,7 @@ defmodule Hexerei.Application do Hexerei.Cache.TranslateCache, [ options: [ - max_size: 512 + max_size: 1024 ] ] }, diff --git a/elixir-api/lib/routes/api/v1/config.ex b/elixir-api/lib/routes/api/v1/config.ex index d98e53646..f1b48057a 100644 --- a/elixir-api/lib/routes/api/v1/config.ex +++ b/elixir-api/lib/routes/api/v1/config.ex @@ -15,7 +15,7 @@ defmodule Router.Api.V1.Config do with {:ok, params} <- validate_query_params(conn, %{"lang" => "en"}) do query = Query.new() - |> Query.filter([%{"_type" => "'siteSettings'"}]) + |> Query.filter([%{"_id" => "'siteSettings'"}]) |> Query.qualify("[0]") |> Query.build!() diff --git a/elixir-api/lib/routes/api/v1/tag.ex b/elixir-api/lib/routes/api/v1/tag.ex index 478de8c3f..b7adb2114 100644 --- a/elixir-api/lib/routes/api/v1/tag.ex +++ b/elixir-api/lib/routes/api/v1/tag.ex @@ -85,16 +85,6 @@ defmodule Router.Api.V1.Tag do } ) - # parsed_counts = Task.await(counts) - - # {transformed_result, meta, code} = - # transform_result_document(query, result, :tag, params, %{ - # "total" => parsed_counts["result"]["total"], - # "count" => parsed_counts["result"]["count"], - # "id" => id, - # "type" => params["type"] - # }) - update_meta_and_send_response(conn, code, transformed_result, meta, duration) end) else diff --git a/elixir-api/lib/routes/cdn.ex b/elixir-api/lib/routes/cdn.ex index 950fe764a..25de2fcb8 100644 --- a/elixir-api/lib/routes/cdn.ex +++ b/elixir-api/lib/routes/cdn.ex @@ -47,11 +47,20 @@ defmodule Router.Cdn do with true <- is_binary(url_parts.filetype), {:ok, url} <- Hexerei.SanityClient.urlFor(url_parts.asset, conn.query_string) do - with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- + with {:ok, %HTTPoison.Response{status_code: 200, body: body, headers: headers}} <- Hexerei.Env.get(:http_client, Hexerei.HTTP.DefaultClient).get(url) do + content_type = + headers + |> Enum.find(fn {k, _} -> String.downcase(k) == "content-type" end) + |> case do + {k, v} -> String.downcase(v) + # fall back to filetype from ext, if present + _ -> "image/#{url_parts.filetype}" + end + conn |> put_resp_header("Access-Control-Allow-Origin", "*") - |> put_resp_content_type("image/#{url_parts.filetype}") + |> put_resp_content_type(content_type) |> send_resp(200, body) else {:ok, %HTTPoison.Response{status_code: 404}} -> diff --git a/elixir-api/lib/utils/translate.ex b/elixir-api/lib/utils/translate.ex index 57dd31e3c..b8aa9f04e 100644 --- a/elixir-api/lib/utils/translate.ex +++ b/elixir-api/lib/utils/translate.ex @@ -30,7 +30,21 @@ defmodule Hexerei.Translate do {:error, "Error parsing response"} end else - {:ok, %HTTPoison.Response{status_code: status_code, body: _body}} -> + {:ok, %HTTPoison.Response{status_code: status_code, body: body}} -> + # try to parse the body to get the error message + parsed_body = + try do + {:ok, parsed_body} = Poison.decode(body) + parsed_body + rescue + _ -> + %{error: %{message: "Unknown error sending request"}} + end + + Logger.error( + "Error sending request - #{status_code} - #{inspect(parsed_body["error"]["message"])}" + ) + {:error, "Error sending request - #{status_code}"} {:error, %HTTPoison.Error{reason: reason}} -> @@ -52,7 +66,8 @@ defmodule Hexerei.Translate do {"q", text_array}, {"source", source_lang}, {"target", target_lang}, - {"format", "text"} + {"format", "text"}, + {"key", Hexerei.Env.get!(:gcloud_key)} ] } end @@ -129,9 +144,9 @@ defmodule Hexerei.Translate do {:ok, translated_blocks} -> translated_blocks - # {:error, reason} -> - # Logger.error("Error translating: #{reason}") - # text_blocks + {:error, errors} -> + Logger.error("Error translating: #{inspect(errors)}") + text_blocks end {:ok, @@ -145,26 +160,83 @@ defmodule Hexerei.Translate do end end + # TODO: Refactor to pattern-match err cases for better readability + # defp translate_config(%{ "result" => nil }, _target_lang, _source_lang) do + # Logger.error("Cannot translate invalid document: #{inspect(sanity_response)}") + # {:error, sanity_response, [%{message: "Cannot translate, invalid document"}]} + # end + defp translate_config(sanity_response, target_lang, source_lang) do with document <- sanity_response["result"], true <- document != nil do + field_names = ["bio"] content_arrays = ["about", "meta"] + # for each field name, fetch value from document, translate, and add to map by original field name + translated_fields = + Enum.reduce(field_names, %{}, fn key, acc -> + case Map.get(document, key) do + value when is_binary(value) -> + translated_field = + case translate_pt_fields([value], target_lang, source_lang) do + {:ok, translated_fields} -> + {:ok, translated_fields} + + {:error, errors} -> + Logger.error("Error(s) translating: #{inspect(errors)}") + # value + {:error, errors} + end + + Map.put(acc, key, translated_field) + + value -> + Logger.warning("Cannot translate invalid field: #{inspect(value)}") + end + end) + translated_content_arrays = Enum.map(content_arrays, fn name -> - translate_content_list(Map.get(document, name), target_lang, source_lang) + case translate_content_list(Map.get(document, name), target_lang, source_lang) do + {:ok, translated_content} -> translated_content + {:error, errors} -> {:error, errors} + end end) - translated_blocks = - Enum.reduce(content_arrays, %{}, fn key, acc -> - idx = content_arrays |> Enum.find_index(fn k -> k == key end) - Map.put(acc, key, Enum.at(translated_content_arrays, idx)) + collected_errors = + Enum.flat_map(translated_content_arrays, fn + {:error, errors} -> errors + _ -> [] end) + |> Enum.concat( + Enum.flat_map(translated_fields, fn + {_key, {:error, errors}} -> errors + _ -> [] + end) + ) + + if not Enum.empty?(collected_errors) do + {:error, sanity_response, collected_errors} + else + translated_blocks = + Enum.reduce(content_arrays, %{}, fn key, acc -> + idx = content_arrays |> Enum.find_index(fn k -> k == key end) + Map.put(acc, key, Enum.at(translated_content_arrays, idx)) + end) + |> (fn acc -> + Enum.reduce(translated_fields, acc, fn {k, v}, acc -> + case v do + {:ok, [translation]} -> Map.put(acc, k, translation) + _ -> Map.put(acc, k, Map.get(document, k)) + end + end) + end).() - {:ok, - %{ - "result" => document |> Map.merge(translated_blocks |> Enum.into(%{})) - }} + {:ok, + %{ + "result" => document |> Map.merge(translated_blocks |> Enum.into(%{})) + }} + end else _ -> Logger.error("Cannot translate invalid document: #{inspect(sanity_response)}") @@ -173,35 +245,53 @@ defmodule Hexerei.Translate do end defp translate_content_list(content_list, target_lang, source_lang) do - Enum.map(content_list, fn content -> - field_names = ["title"] - block_names = ["content"] + updated_list = + Enum.map(content_list, fn content -> + field_names = ["title"] + block_names = ["content"] + + fields = Enum.map(field_names, &Map.get(content, &1)) + blocks = Map.take(content, block_names) + + translated_title = + case translate_pt_fields(fields, target_lang, source_lang) do + {:ok, translated_fields} -> + translated_fields |> List.first() + + {:error, errors} -> + Logger.error("Error translating: #{inspect(errors |> List.first())}") + content["title"] + end - fields = Enum.map(field_names, &Map.get(content, &1)) - blocks = Map.take(content, block_names) + translated_content = + case translate_pt_blocks(blocks, target_lang, source_lang) do + {:ok, translated_blocks} -> + translated_blocks["content"] - translated_title = - case translate_pt_fields(fields, target_lang, source_lang) do - {:ok, translated_fields} -> - translated_fields |> List.first() + {:error, errors} -> + Logger.error( + "Error(s) translating config_content_list blocks: #{inspect(errors |> List.first())}" + ) - # {:error, reason} -> - # Logger.error("Error translating: #{reason}") - # content["title"] - end + {:error, errors} + end - translated_content = - case translate_pt_blocks(blocks, target_lang, source_lang) do - {:ok, translated_blocks} -> - translated_blocks["content"] + case translated_content do + {:error, errors} -> + %{"title" => translated_title, "content" => content["content"], "errors" => errors} - # {:error, reason} -> - # Logger.error("Error translating: #{reason}") - # content["content"] + _ -> + %{"title" => translated_title, "content" => translated_content, "errors" => []} end + end) - %{"title" => translated_title, "content" => translated_content} - end) + all_errors = Enum.flat_map(updated_list, fn content -> content["errors"] end) + + if Enum.empty?(all_errors) do + {:ok, updated_list |> Enum.map(fn content -> Map.delete(content, "errors") end)} + else + {:error, all_errors} + end end defp translate_post(sanity_response, target_lang, source_lang, ignore_blocks \\ false) do @@ -218,50 +308,55 @@ defmodule Hexerei.Translate do {:ok, translated_fields} -> translated_fields - # {:error, reason} -> - # Logger.error("Error translating: #{reason}") - # text_fields + {:error, errors} -> + {:error, errors} end - # Since field translations will be in reverse order we can just pop them off - updated_document = - Enum.reduce(field_names, document, fn key, acc -> - original_value = Map.get(acc, key) - index = field_names |> Enum.find_index(fn k -> k == key end) - translation = Enum.at(translated_fields, index) - - translation = - case translation do - [translation] -> translation - translation when is_binary(translation) -> translation - # Gracefully fall back to original value if translation is invalid - _ -> original_value - end - - Map.put(acc, key, translation) - end) + case translated_fields do + {:error, errors} -> + {:error, sanity_response, errors} - # For lists of posts we don't want to translate the blocks since they aren't visible anyways - if ignore_blocks do - {:ok, - %{ - "result" => updated_document - }} - else - translated_blocks = - case translate_pt_blocks(text_blocks, target_lang, source_lang) do - {:ok, translated_blocks} -> - translated_blocks + _ -> + # Since field translations will be in reverse order we can just pop them off + updated_document = + Enum.reduce(field_names, document, fn key, acc -> + original_value = Map.get(acc, key) + index = field_names |> Enum.find_index(fn k -> k == key end) + translation = Enum.at(translated_fields, index) + + translation = + case translation do + [translation] -> translation + translation when is_binary(translation) -> translation + # Gracefully fall back to original value if translation is invalid + _ -> original_value + end + + Map.put(acc, key, translation) + end) - # {:error, reason} -> - # Logger.error("Error translating post blocks: #{reason}") - # text_blocks - end + # For lists of posts we don't want to translate the blocks since they aren't visible anyways + if ignore_blocks do + {:ok, + %{ + "result" => updated_document + }} + else + translated_blocks = + case translate_pt_blocks(text_blocks, target_lang, source_lang) do + {:ok, translated_blocks} -> + translated_blocks + + {:error, errors} -> + Logger.error("Error(s) translating post blocks: #{inspect(errors)}") + text_blocks + end - {:ok, - %{ - "result" => updated_document |> Map.merge(translated_blocks |> Enum.into(%{})) - }} + {:ok, + %{ + "result" => updated_document |> Map.merge(translated_blocks |> Enum.into(%{})) + }} + end end else _ -> @@ -273,40 +368,32 @@ defmodule Hexerei.Translate do defp translate_posts(sanity_response, target_lang, source_lang) do documents = sanity_response["result"] - updated_docs = + updated_docs_with_errors = Enum.map(documents, fn document -> - translate_post(%{"result" => document}, target_lang, source_lang) - end) + case translate_post(%{"result" => document}, target_lang, source_lang, true) do + {:ok, translated_doc} -> + {:ok, translated_doc["result"], []} - {left, right} = - Enum.split_with(updated_docs, fn doc -> - case doc do - {:ok, _} -> true - {:error, _, _} -> false + {:error, _doc, errors} -> + {:error, document, errors} end end) - if Enum.empty?(right) do - { - :ok, - %{ - "result" => - Enum.map(left, fn item -> - {_status, doc} = item - doc["result"] - end) - } - } - else - {all_docs, errors} = - Enum.reduce(updated_docs, {[], []}, fn item, {acc, errors} -> - case item do - {:ok, doc} -> {[doc["result"] | acc], errors} - {:error, doc, doc_errors} -> {[doc["result"] | acc], [errors | doc_errors]} - end - end) + {successful, failed} = + Enum.split_with(updated_docs_with_errors, fn + {:ok, _doc, _errors} -> true + {:error, _doc, _errors} -> false + end) + + translated_results = Enum.map(successful, fn {:ok, doc, _errors} -> doc end) + failed_results = Enum.map(failed, fn {:error, doc, _errors} -> doc end) + + errors = Enum.flat_map(failed, fn {:error, _doc, errors} -> errors end) - {:error, %{"result" => all_docs}, errors} + if Enum.empty?(errors) do + {:ok, %{"result" => translated_results}} + else + {:error, %{"result" => translated_results ++ failed_results}, errors} end end @@ -324,38 +411,43 @@ defmodule Hexerei.Translate do {:ok, translated_fields} -> translated_fields - # {:error, reason} -> - # Logger.error("Error translating: #{reason}") - # text_fields + {:error, errors} -> + {:error, errors} end - updated_document = - Enum.reduce(field_names, document, fn key, acc -> - index = field_names |> Enum.find_index(fn k -> k == key end) - {_list, [translation]} = translated_fields |> List.pop_at(index) - Map.put(acc, key, translation |> List.first()) - end) + case translated_fields do + {:error, errors} -> + {:error, sanity_response, errors} - if ignore_blocks do - {:ok, - %{ - "result" => updated_document - }} - else - translated_blocks = - case translate_pt_blocks(text_blocks, target_lang, source_lang) do - {:ok, translated_blocks} -> - translated_blocks + _ -> + updated_document = + Enum.reduce(field_names, document, fn key, acc -> + index = field_names |> Enum.find_index(fn k -> k == key end) + {_list, [translation]} = translated_fields |> List.pop_at(index) + Map.put(acc, key, translation |> List.first()) + end) - # {:error, reason} -> - # Logger.error("Error translating: #{reason}") - # text_blocks - end + if ignore_blocks do + {:ok, + %{ + "result" => updated_document + }} + else + translated_blocks = + case translate_pt_blocks(text_blocks, target_lang, source_lang) do + {:ok, translated_blocks} -> + translated_blocks + + {:error, errors} -> + Logger.error("Error(s) translating project blocks: #{inspect(errors)}") + text_blocks + end - {:ok, - %{ - "result" => updated_document |> Map.merge(translated_blocks |> Enum.into(%{})) - }} + {:ok, + %{ + "result" => updated_document |> Map.merge(translated_blocks |> Enum.into(%{})) + }} + end end else _ -> @@ -367,40 +459,32 @@ defmodule Hexerei.Translate do defp translate_projects(sanity_response, target_lang, source_lang) do documents = sanity_response["result"] - updated_documents = + updated_docs_with_errors = Enum.map(documents, fn document -> - translate_project(%{"result" => document}, target_lang, source_lang, true) - end) + case translate_project(%{"result" => document}, target_lang, source_lang, true) do + {:ok, translated_doc} -> + {:ok, translated_doc["result"], []} - {left, right} = - Enum.split_with(updated_documents, fn doc -> - case doc do - {:ok, _} -> true - {:error, _, _} -> false + {:error, _doc, errors} -> + {:error, document, errors} end end) - if Enum.empty?(right) do - { - :ok, - %{ - "result" => - Enum.map(left, fn item -> - {_status, doc} = item - doc["result"] - end) - } - } - else - {all_docs, errors} = - Enum.reduce(updated_documents, {[], []}, fn item, {acc, errors} -> - case item do - {:ok, doc} -> {[doc["result"] | acc], errors} - {:error, doc, doc_errors} -> {[doc["result"] | acc], [errors | doc_errors]} - end - end) + {successful, failed} = + Enum.split_with(updated_docs_with_errors, fn + {:ok, _doc, _errors} -> true + {:error, _doc, _errors} -> false + end) + + translated_results = Enum.map(successful, fn {:ok, doc, _errors} -> doc end) + failed_results = Enum.map(failed, fn {:error, doc, _errors} -> doc end) - {:error, %{"result" => all_docs}, errors} + errors = Enum.flat_map(failed, fn {:error, _doc, errors} -> errors end) + + if Enum.empty?(errors) do + {:ok, %{"result" => translated_results}} + else + {:error, %{"result" => translated_results ++ failed_results}, errors} end end @@ -415,14 +499,25 @@ defmodule Hexerei.Translate do result {:ok, {:error, reason}} -> - {:error, "Error translating fields: #{inspect(reason)}"} + Logger.error("Error translating fields: #{inspect(reason)}") + {:error, reason} {:exit, reason} -> Logger.error("Error translating fields: #{inspect(reason)}") {:error, "Error translating fields"} end) - {:ok, translations} + collected_errors = + Enum.flat_map(translations, fn + {:error, reason} -> [reason] + _ -> [] + end) + + if not Enum.empty?(collected_errors) do + {:error, collected_errors} + else + {:ok, translations} + end end defp translate_pt_blocks(pt_blocks, target_lang, source_lang) do @@ -432,7 +527,7 @@ defmodule Hexerei.Translate do |> Enum.map(fn {key, value} -> case value do nil -> - {key, nil} + {key, nil, []} _ -> text_array = get_text_from_blocks(value) @@ -447,21 +542,33 @@ defmodule Hexerei.Translate do {:ok, {:ok, result}} -> result - {:ok, {:error, _}} -> - {:error, "Error translating block"} + {:ok, {:error, reason}} -> + {:error, "Error translating block: #{inspect(reason)}"} {:exit, reason} -> Logger.error("Error translating block: #{inspect(reason)}") {:error, "Error translating block"} end) + collected_errors = + Enum.flat_map(translations, fn + {:error, reason} -> [reason] + _ -> [] + end) + updated_blocks = replace_text_in_children(value, text_array, translations) - {key, updated_blocks} + {key, updated_blocks, collected_errors} end end) - {:ok, updated_fields |> Enum.into(%{})} + all_errors = Enum.flat_map(updated_fields, fn {_, _, errors} -> errors end) + + if Enum.empty?(all_errors) do + {:ok, Enum.map(updated_fields, fn {key, value, _} -> {key, value} end) |> Enum.into(%{})} + else + {:error, all_errors} + end end defp translate_field(text, target_lang, source_lang) do @@ -506,41 +613,117 @@ defmodule Hexerei.Translate do |> Enum.reject(&is_nil/1) end - defp replace_text_in_children(blocks, original_text, translations) do - # Build a map from original_text to translations for quick lookup - translation_map = original_text |> Enum.zip(translations) |> Map.new() + defp replace_text_in_children(blocks, original_texts, translations) do + # Build a list of text entries with their whitespace + texts_with_ws = + original_texts + |> Enum.map(fn text -> + {leading_ws, trimmed_text, trailing_ws} = extract_whitespace(text) - Enum.map(blocks, fn block -> - case Map.fetch(block, "children") do - {:ok, children} -> - updated_children = - Enum.map(children, fn child -> - case child do - # Check if the child is a text node and it's not marked w/ "notranslate" - %{"text" => text, "marks" => marks} when text != nil -> - if Enum.member?(marks || [], "notranslate") or Enum.member?(marks || [], "code") do - child - else - translated = - case translation_map[text] do - # Unwrap single-item list - [single_translation] -> single_translation - _ -> text - end - - Map.put(child, "text", translated) - end + %{ + original_text: text, + trimmed_text: trimmed_text, + leading_ws: leading_ws, + trailing_ws: trailing_ws + } + end) - _ -> - child - end - end) + # Re-attach whitespace to translations + translations_with_ws = + texts_with_ws + |> Enum.zip(translations) + |> Enum.map(fn {text_info, translation} -> + # Unwrap single-item list + translation_string = + case translation do + [single_translation] -> single_translation + translation when is_binary(translation) -> translation + _ -> "" + end - Map.put(block, "children", updated_children) + new_text = text_info.leading_ws <> translation_string <> text_info.trailing_ws + {text_info.original_text, new_text} + end) - :error -> - block - end + replace_text_in_blocks(blocks, Map.new(translations_with_ws)) + end + + defp replace_text_in_blocks(blocks, translation_map) do + Enum.map(blocks, fn block -> + update_block(block, translation_map) end) end + + defp update_block(%{"children" => children} = block, translation_map) do + updated_children = + Enum.map(children, fn child -> + update_child(child, translation_map) + end) + + Map.put(block, "children", updated_children) + end + + defp update_block(block, _translation_map), do: block + + defp update_child(%{"text" => text, "marks" => marks} = child, translation_map) + when is_binary(text) do + marks_list = marks || [] + + if Enum.any?(marks_list, &(&1 == "notranslate" or &1 == "code")) do + child + else + Map.put(child, "text", Map.get(translation_map, text, text)) + end + end + + defp update_child(child, _translation_map), do: child + + defp extract_whitespace(text) when is_binary(text) do + trimmed_text = String.trim(text) + leading_ws_length = String.length(text) - String.length(String.trim_leading(text)) + trailing_ws_length = String.length(text) - String.length(String.trim_trailing(text)) + leading_ws = String.slice(text, 0, leading_ws_length) || "" + trailing_ws = String.slice(text, -trailing_ws_length, trailing_ws_length) || "" + {leading_ws, trimmed_text, trailing_ws} + end + + defp extract_whitespace(_), do: {"", "", ""} + + # defp replace_text_in_children(blocks, original_text, translations) do + # # Build a map from original_text to translations for quick lookup + # translation_map = original_text |> Enum.zip(translations) |> Map.new() + + # Enum.map(blocks, fn block -> + # case Map.fetch(block, "children") do + # {:ok, children} -> + # updated_children = + # Enum.map(children, fn child -> + # case child do + # # Check if the child is a text node and it's not marked w/ "notranslate" + # %{"text" => text, "marks" => marks} when text != nil -> + # if Enum.member?(marks || [], "notranslate") or Enum.member?(marks || [], "code") do + # child + # else + # translated = + # case translation_map[text] do + # # Unwrap single-item list + # [single_translation] -> single_translation + # _ -> text + # end + + # Map.put(child, "text", translated) + # end + + # _ -> + # child + # end + # end) + + # Map.put(block, "children", updated_children) + + # :error -> + # block + # end + # end) + # end end diff --git a/elixir-api/lib/utils/utils.ex b/elixir-api/lib/utils/utils.ex index 4229038cb..8f373c43d 100644 --- a/elixir-api/lib/utils/utils.ex +++ b/elixir-api/lib/utils/utils.ex @@ -172,6 +172,43 @@ defmodule Hexerei.Utils do } end + (meta["count"] > 0 and type == :posts) or type == :projects -> + # for each post/project, build a summary + {headings_by_item, heading_errors} = + Enum.reduce( + translated_result["result"], + {%{}, []}, + fn item, {acc, errors} -> + case PT.build_summary(item["body"]) do + {:error, headings, err} -> + {Map.put(acc, item["_id"], headings), [err | errors]} + + {:ok, headings} -> + {Map.put(acc, item["_id"], headings), errors} + end + end + ) + + if Map.keys(headings_by_item) |> Enum.empty?() do + {translated_result, [translate_errors | heading_errors]} + else + { + Kernel.put_in( + translated_result, + ["result"], + translated_result["result"] + |> Enum.map(fn item -> + Kernel.put_in( + item, + ["headings"], + Map.get(headings_by_item, item["_id"]) + ) + end) + ), + [translate_errors | heading_errors] + } + end + true -> {translated_result, translate_errors} end diff --git a/sanity-cms/schemas/settings.ts b/sanity-cms/schemas/settings.ts index 11260aa89..72af228df 100644 --- a/sanity-cms/schemas/settings.ts +++ b/sanity-cms/schemas/settings.ts @@ -26,12 +26,137 @@ export default { title: 'Site Settings', type: 'document', groups: [ + { + name: 'sidebar', + title: 'Sidebar' + }, { name: 'sections', title: 'Sections' } ], fields: [ + { + name: 'name', + type: 'string', + title: 'Name', + validation: (Rule: Rule) => Rule.required(), + group: 'sidebar' + }, + { + name: 'image', + type: 'object', + title: 'Profile Picture', + validation: (Rule: Rule) => Rule.required(), + fields: [ + { + name: 'dark', + type: 'image', + title: 'Dark Mode', + validation: (Rule: Rule) => Rule.required() + }, + { + name: 'light', + type: 'image', + title: 'Light Mode', + validation: (Rule: Rule) => Rule.required() + } + ], + group: 'sidebar' + }, + { + name: 'handle', + type: 'string', + title: 'Handle', + group: 'sidebar' + }, + { + name: 'bio', + type: 'text', + title: 'Bio', + group: 'sidebar' + }, + { + name: 'enableToru', + type: 'boolean', + title: 'Enable Toru', + group: 'sidebar', + initialValue: false, + // options: { + // layout: + // }, + validation: (Rule: Rule) => Rule.required() + }, + { + name: 'socialLinks', + type: 'array', + title: 'Social Links', + of: [ + { + title: 'Social', + type: 'object', + fields: [ + { + name: 'name', + title: 'Name', + type: 'string', + validation: (Rule: Rule) => Rule.required() + }, + { + name: 'url', + title: 'URL', + type: 'string', + validation: (Rule: Rule) => { + return Rule.uri({ + allowRelative: true, + scheme: ['https', 'http', 'mailto', 'tel'] + }); + } + }, + { + name: 'internal', + title: 'Internal', + type: 'boolean', + initialValue: false, + options: { + layout: 'checkbox' + }, + description: 'Relative to the site root, e.g. /about' + }, + { + name: 'rel', + title: 'Rel', + type: 'array', + of: [ + { + type: 'string', + options: { + list: [ + { title: 'nofollow', value: 'nofollow' }, + { title: 'noopener', value: 'noopener' }, + { title: 'noreferrer', value: 'noreferrer' }, + { title: 'me', value: 'me' } + ] + } + } + ] + } + ], + preview: { + select: { + title: 'name', + subtitle: 'url' + }, + prepare: ({ title, subtitle }) => + ({ + title, + subtitle + }) as PreviewValue + } satisfies PreviewConfig + } + ], + group: 'sidebar' + }, { name: 'about', type: 'array', @@ -111,75 +236,6 @@ export default { } ] }, - { - name: 'socialLinks', - type: 'array', - title: 'Social Links', - of: [ - { - title: 'Social', - type: 'object', - fields: [ - { - name: 'name', - title: 'Name', - type: 'string', - validation: (Rule: Rule) => Rule.required() - }, - { - name: 'url', - title: 'URL', - type: 'string', - validation: (Rule: Rule) => { - return Rule.uri({ - allowRelative: true, - scheme: ['https', 'http', 'mailto', 'tel'] - }); - } - }, - { - name: 'internal', - title: 'Internal', - type: 'boolean', - initialValue: false, - options: { - layout: 'checkbox' - }, - description: 'Relative to the site root, e.g. /about' - }, - { - name: 'rel', - title: 'Rel', - type: 'array', - of: [ - { - type: 'string', - options: { - list: [ - { title: 'nofollow', value: 'nofollow' }, - { title: 'noopener', value: 'noopener' }, - { title: 'noreferrer', value: 'noreferrer' }, - { title: 'me', value: 'me' } - ] - } - } - ] - } - ], - preview: { - select: { - title: 'name', - subtitle: 'url' - }, - prepare: ({ title, subtitle }) => - ({ - title, - subtitle - }) as PreviewValue - } satisfies PreviewConfig - } - ] - }, { name: 'pgpKey', type: 'text', diff --git a/svelte-app/.eslintrc b/svelte-app/.eslintrc index a24a2b45c..b93904ec7 100644 --- a/svelte-app/.eslintrc +++ b/svelte-app/.eslintrc @@ -16,14 +16,16 @@ "plugin:svelte/recommended", "plugin:@typescript-eslint/recommended", "plugin:import/recommended", - "plugin:import/typescript" + "plugin:import/typescript", + "plugin:local-rules/all" ], "plugins": [ "prettier", "import", "@typescript-eslint", "simple-import-sort", - "unused-imports" + "unused-imports", + "local-rules" ], "rules": { "@typescript-eslint/prefer-namespace-keyword": ["off"], @@ -156,6 +158,9 @@ "parser": "svelte-eslint-parser", "parserOptions": { "parser": "@typescript-eslint/parser" + }, + "rules": { + "local-rules/no-bare-strings": ["error"] } }, { diff --git a/svelte-app/eslint-local-rules.cjs b/svelte-app/eslint-local-rules.cjs new file mode 100644 index 000000000..e92c99942 --- /dev/null +++ b/svelte-app/eslint-local-rules.cjs @@ -0,0 +1,96 @@ +const allowedSpecialChars = [ + ' ', + '•', + '•', + '#', + '&', + '...', + '[', + ']', + '(', + ')', + ':', + ';', + '/' +]; + +const htmlEntityRegex = /^&[a-zA-Z]+;$/; + +module.exports = { + 'no-bare-strings': { + meta: { + type: 'problem', + docs: { + description: 'Disallow bare strings in Svelte component templates', + category: 'Best Practices' + }, + fixable: 'code', + schema: [] + }, + create(context) { + const elementStack = []; + + return { + SvelteElement(node) { + if (node.name && node.name.name) { + elementStack.push(node.name.name); + } else { + elementStack.push(null); + } + }, + 'SvelteElement:exit'() { + elementStack.pop(); + }, + SvelteText(node) { + const text = node.value; + + if (!/\S/.test(text)) { + return; + } + + if (elementStack.includes('script') || elementStack.includes('style')) { + return; + } + + const trimmedText = text.trim(); + + if (trimmedText.length === 1 || !/[\w]/.test(trimmedText)) { + return; + } + + if (allowedSpecialChars.includes(trimmedText)) { + return; + } + + if (htmlEntityRegex.test(trimmedText)) { + return; + } + + const parent = node.parent; + + if ( + parent.type === 'SvelteMustacheTag' && + parent.expression.type === 'CallExpression' && + parent.expression.callee.name === '$t' + ) { + return; + } + + if (parent.type === 'SvelteAttribute' || parent.type === 'SvelteStyleElement') { + return; + } + + context.report({ + node, + message: 'Bare strings are not allowed in templates. Use $t(...) instead.', + fix(fixer) { + const escapedText = text.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const replacement = `{$t('${escapedText}')}`; + return fixer.replaceText(node, replacement); + } + }); + } + }; + } + } +}; diff --git a/svelte-app/package.json b/svelte-app/package.json index bb3818059..b954d31ad 100644 --- a/svelte-app/package.json +++ b/svelte-app/package.json @@ -21,6 +21,7 @@ "lint": "pnpm format && pnpm eslint --fix \"./src/**/*.{ts,svelte}\" \"./types/**/*.ts\"" }, "devDependencies": { + "@floating-ui/dom": "^1.6.11", "@playwright/test": "1.44.1", "@portabletext/svelte": "2.1.11", "@portabletext/toolkit": "2.0.15", @@ -38,6 +39,7 @@ "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", + "eslint-plugin-local-rules": "^3.0.2", "eslint-plugin-prettier": "5.1.3", "eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-svelte": "2.43.0", diff --git a/svelte-app/pnpm-lock.yaml b/svelte-app/pnpm-lock.yaml index f7ccc2374..8c105ac63 100644 --- a/svelte-app/pnpm-lock.yaml +++ b/svelte-app/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@floating-ui/dom': + specifier: ^1.6.11 + version: 1.6.11 '@playwright/test': specifier: 1.44.1 version: 1.44.1 @@ -59,6 +62,9 @@ importers: eslint-plugin-import: specifier: 2.29.1 version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) + eslint-plugin-local-rules: + specifier: ^3.0.2 + version: 3.0.2 eslint-plugin-prettier: specifier: 5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.2) @@ -660,6 +666,15 @@ packages: '@fastify/static@7.0.4': resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + + '@floating-ui/dom@1.6.11': + resolution: {integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==} + + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2587,6 +2602,9 @@ packages: '@typescript-eslint/parser': optional: true + eslint-plugin-local-rules@3.0.2: + resolution: {integrity: sha512-IWME7GIYHXogTkFsToLdBCQVJ0U4kbSuVyDT+nKoR4UgtnVrrVeNWuAZkdEu1nxkvi9nsPccGehEEF6dgA28IQ==} + eslint-plugin-prettier@5.1.3: resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6377,6 +6395,17 @@ snapshots: fastq: 1.17.1 glob: 10.4.1 + '@floating-ui/core@1.6.8': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.11': + dependencies: + '@floating-ui/core': 1.6.8 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/utils@0.2.8': {} + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -8604,6 +8633,8 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-local-rules@3.0.2: {} + eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.2): dependencies: eslint: 8.57.0 diff --git a/svelte-app/src/components/about/timeline-item.svelte b/svelte-app/src/components/about/timeline-item.svelte index 3cdd94b5a..b995df96e 100644 --- a/svelte-app/src/components/about/timeline-item.svelte +++ b/svelte-app/src/components/about/timeline-item.svelte @@ -1,6 +1,7 @@ -
+

{title}

- {$displayRange(section[section.length - 1].range.start, section[0].range.end)} • - {$displayMonthDuration( - section[section.length - 1].range.start, - section[0].range.end - )} + {$displayRange( + section[section.length - 1].range.start, + section[0].range.end + )} + {$displayMonthDuration( + section[section.length - 1].range.start, + section[0].range.end + )}

diff --git a/svelte-app/src/components/code-block.svelte b/svelte-app/src/components/code-block.svelte index 7d483ab86..ffd80a76f 100644 --- a/svelte-app/src/components/code-block.svelte +++ b/svelte-app/src/components/code-block.svelte @@ -1,14 +1,20 @@
{#if filename}
- {filename} + + {filename}
{/if} - + - +

{content}

@@ -163,11 +168,12 @@ {#if hideLoader && innerHeight > DEFAULT_CODE_BLOCK_HEIGHT}
{/if} @@ -192,10 +201,10 @@ @import '@styles/mixins'; .show-more-gradient { - background: ease-gradient('to top', $neutral-0, transparent); + background: ease-gradient('to top', $neutral-200, transparent); @include dark { - background: ease-gradient('to top', $neutral-800, transparent); + background: ease-gradient('to top', $neutral-700, transparent); } } diff --git a/svelte-app/src/components/controls/arrow-button.svelte b/svelte-app/src/components/controls/arrow-button.svelte index 7b5500b22..d2b283feb 100644 --- a/svelte-app/src/components/controls/arrow-button.svelte +++ b/svelte-app/src/components/controls/arrow-button.svelte @@ -1,7 +1,14 @@ - - -
- {#if placement === 'after'} - {#if $$slots.default} - - {:else} - {text} - {/if} + + {#if placement === 'after'} + + {#if $$slots.default} + + {:else} + {text} {/if} -
- {#if dir === 'left'} - ← - {:else if dir === 'right'} - → - {:else if dir === 'up'} - ↑ - {:else if dir === 'down'} - ↓ - {/if} -
- {#if placement === 'before'} - {#if $$slots.default} - - {:else} - {text} - {/if} +
+ {/if} + + +
-
-
+ + {/if} + diff --git a/svelte-app/src/components/controls/base-toggle.svelte b/svelte-app/src/components/controls/base-toggle.svelte new file mode 100644 index 000000000..0404e27c8 --- /dev/null +++ b/svelte-app/src/components/controls/base-toggle.svelte @@ -0,0 +1,39 @@ + + + diff --git a/svelte-app/src/components/controls/lang-toggle.svelte b/svelte-app/src/components/controls/lang-toggle.svelte new file mode 100644 index 000000000..8c0f6e399 --- /dev/null +++ b/svelte-app/src/components/controls/lang-toggle.svelte @@ -0,0 +1,47 @@ + + + diff --git a/svelte-app/src/components/controls/language-toggle.svelte b/svelte-app/src/components/controls/language-toggle.svelte deleted file mode 100644 index 381c4b8eb..000000000 --- a/svelte-app/src/components/controls/language-toggle.svelte +++ /dev/null @@ -1,126 +0,0 @@ - - - - -
- get(isMobile) && - handleClick(e, $currentLang === APP_LANGS[0] ? APP_LANGS[1] : APP_LANGS[0])} - on:keyup={(e) => - e.key === 'Enter' && - get(isMobile) && - handleClick(e, $currentLang === APP_LANGS[0] ? APP_LANGS[1] : APP_LANGS[0])} - role="menu" - aria-label="Change language" - tabindex="0" -> - - [{$currentLang === APP_LANGS[0] ? 'en' : 'fr'}] - - {#if showDropdown} -
- {#each dropdownOptions as opt} - - - - {/each} -
- {/if} -
diff --git a/svelte-app/src/components/controls/theme-toggle.svelte b/svelte-app/src/components/controls/theme-toggle.svelte index 0954a148d..2c1d7a3e2 100644 --- a/svelte-app/src/components/controls/theme-toggle.svelte +++ b/svelte-app/src/components/controls/theme-toggle.svelte @@ -3,26 +3,30 @@ import { t } from '$lib/i18n'; import Settings from '$lib/settings'; - import Hoverable from '$components/hoverable.svelte'; + import BaseToggle from '$components/controls/base-toggle.svelte'; + import MoonSmall from '$components/icons/moon-small.svelte'; + import SunSmall from '$components/icons/sun-small.svelte'; const { theme, modified } = Settings; - - - + { + modified.set(true); + theme.set($theme === APP_THEMES.LIGHT ? APP_THEMES.DARK : APP_THEMES.LIGHT); + }} +/> diff --git a/svelte-app/src/components/divider.svelte b/svelte-app/src/components/divider.svelte index 4e2c73b0e..767181c9d 100644 --- a/svelte-app/src/components/divider.svelte +++ b/svelte-app/src/components/divider.svelte @@ -3,6 +3,6 @@ diff --git a/svelte-app/src/components/document/content/content.svelte b/svelte-app/src/components/document/content/content.svelte index 23c270813..0ed371a18 100644 --- a/svelte-app/src/components/document/content/content.svelte +++ b/svelte-app/src/components/document/content/content.svelte @@ -2,6 +2,7 @@ import Footer from '$components/document/content/footer.svelte'; import Header from '$components/document/content/header.svelte'; import EmptyContent from '$components/empty-content.svelte'; + import BaseContainer from '$components/layouts/base-container.svelte'; import PortableText from '$components/portable-text/portable-text.svelte'; import type { PostDocument, ProjectDocument, ProjectImage, RouteFetch } from '$types'; @@ -9,15 +10,17 @@ export let data: PostDocument | ProjectDocument, images: ProjectImage[] | undefined = undefined, model = data._type, - routeFetch: RouteFetch | undefined = undefined; + routeFetch: RouteFetch; -
-
- {#if data.body} - - {:else} - - {/if} +
+
+ + {#if data.body} + + {:else} + + {/if} + +
-
diff --git a/svelte-app/src/components/document/content/footer.svelte b/svelte-app/src/components/document/content/footer.svelte index 768eafe78..2651f1f2d 100644 --- a/svelte-app/src/components/document/content/footer.svelte +++ b/svelte-app/src/components/document/content/footer.svelte @@ -3,6 +3,7 @@ import { isMobile } from '$lib/responsive'; import ArrowButton from '$components/controls/arrow-button.svelte'; + import BaseContainer from '$components/layouts/base-container.svelte'; import Link from '$components/link.svelte'; import type { PostDocument, ProjectDocument } from '$types'; @@ -12,27 +13,32 @@ {#if data._type === 'project' && data.github} -
-

{$t('Links')}

- - git / - - {'github.com/' + data.github.split('github.com/')?.[1]} - - -
+ +
+

{$t('Links')}

+ + + git + / + + + {'github.com/' + data.github.split('github.com/')?.[1]} + + +
+
{/if} -
- -
+ +
+ +
+
diff --git a/svelte-app/src/components/document/content/header.svelte b/svelte-app/src/components/document/content/header.svelte index 7cab77bd0..cca97c006 100644 --- a/svelte-app/src/components/document/content/header.svelte +++ b/svelte-app/src/components/document/content/header.svelte @@ -1,11 +1,19 @@ -
-

- {data.title} -

- -
-
+ +

+ {$formatDate(data.date, 'full') ?? $t('Unknown date')} +

+
+ - -

- {$formatDate(data.date, 'full') ?? $t('Unknown date')} -

-
- -

+

{$t('{length} min read', { length: data.estimatedReadingTime ?? 0 })}

- -

- {$t('{views} views', { views: $parseViews((data.views ?? 0) + 1) })} -

- {#if data._type === 'project' && data.githubStars !== undefined && data.githubStars > 0} - -

- {$t('{stars} stars', { stars: $parseViews(data.githubStars) })} -

- {/if} -
- {#if !data.tags?.length} -
+ + - {#if data.tags?.length} -
+
+
+

+ {data.title} +

+ {#if data.desc} +

{data.desc}

+ {/if} + {#if data._type === 'project' && data.github}
- {#each data.tags as tag} - - {tag.title.toLowerCase()} - - {/each} + + git + / + + + + {'github.com/' + data.github.split('github.com/')?.[1]} + +
-
+ + {#if data.tags?.length} +
+ {#each data.tags as tag} + + # + {tag.title.toLowerCase()} + + {/each}
{/if}
-{#if data._type === 'project' && data.github} -
- url / - - - {'github.com/' + data.github.split('github.com/')?.[1]} - - -
-{/if} - {#if data._type === 'project' && images?.length} -
- {#if images.length > 1} - - {:else} - - {/if} -
+ +
+ {#if images.length > 1} + + {:else} + + {/if} +
+
{/if} diff --git a/svelte-app/src/components/header.svelte b/svelte-app/src/components/header.svelte deleted file mode 100644 index 52721d154..000000000 --- a/svelte-app/src/components/header.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - -
-
- -
- - -
-
-
- - diff --git a/svelte-app/src/components/headings/headed-block.svelte b/svelte-app/src/components/headings/headed-block.svelte index 8dd6c5249..98d4dd7f9 100644 --- a/svelte-app/src/components/headings/headed-block.svelte +++ b/svelte-app/src/components/headings/headed-block.svelte @@ -2,14 +2,21 @@ import ConstrainWidth from '$components/layouts/constrain-width.svelte'; export let heading: string | undefined = undefined, + first = false, constrainWidth = true, testId: string | undefined = undefined; export const id = Math.random().toString(36).substring(2); -
-
+
+
{#if $$slots.heading} {:else} diff --git a/svelte-app/src/components/hoverable.svelte b/svelte-app/src/components/hoverable.svelte index 74f5e46b3..07ce2c2bb 100644 --- a/svelte-app/src/components/hoverable.svelte +++ b/svelte-app/src/components/hoverable.svelte @@ -1,42 +1,65 @@ handleHoverIn()} - on:mouseout={() => handleHoverOut()} - on:focus={() => handleHoverIn()} - on:focusin={() => handleHoverIn()} - on:mouseenter={() => handleHoverIn()} - on:mouseleave={() => handleHoverOut()} - on:focusout={() => handleHoverOut()} - on:blur={() => handleHoverOut()} + on:mouseover={handleHoverIn} + on:focus={handleHoverIn} + on:focusin={handleHoverIn} + on:mouseenter={handleHoverIn} + on:mouseleave={handleHoverOut} + on:focusout={handleHoverOut} + on:blur={handleHoverOut} role="none" > diff --git a/svelte-app/src/components/icons/arrow-down-small.svelte b/svelte-app/src/components/icons/arrow-down-small.svelte new file mode 100644 index 000000000..b9c9fa320 --- /dev/null +++ b/svelte-app/src/components/icons/arrow-down-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/arrow-left-small.svelte b/svelte-app/src/components/icons/arrow-left-small.svelte new file mode 100644 index 000000000..0024bc3dc --- /dev/null +++ b/svelte-app/src/components/icons/arrow-left-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/arrow-right-small.svelte b/svelte-app/src/components/icons/arrow-right-small.svelte new file mode 100644 index 000000000..24399f4a4 --- /dev/null +++ b/svelte-app/src/components/icons/arrow-right-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/arrow-up-small.svelte b/svelte-app/src/components/icons/arrow-up-small.svelte new file mode 100644 index 000000000..6f6bcffc8 --- /dev/null +++ b/svelte-app/src/components/icons/arrow-up-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-double-down-small.svelte b/svelte-app/src/components/icons/chevron-double-down-small.svelte new file mode 100644 index 000000000..e5ec5f5a0 --- /dev/null +++ b/svelte-app/src/components/icons/chevron-double-down-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-double-left-small.svelte b/svelte-app/src/components/icons/chevron-double-left-small.svelte new file mode 100644 index 000000000..aedf5d022 --- /dev/null +++ b/svelte-app/src/components/icons/chevron-double-left-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-double-right-small.svelte b/svelte-app/src/components/icons/chevron-double-right-small.svelte new file mode 100644 index 000000000..eac8b1ebf --- /dev/null +++ b/svelte-app/src/components/icons/chevron-double-right-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-double-up-small.svelte b/svelte-app/src/components/icons/chevron-double-up-small.svelte new file mode 100644 index 000000000..4dac0ed10 --- /dev/null +++ b/svelte-app/src/components/icons/chevron-double-up-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-down-small.svelte b/svelte-app/src/components/icons/chevron-down-small.svelte new file mode 100644 index 000000000..950f3b7c8 --- /dev/null +++ b/svelte-app/src/components/icons/chevron-down-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-left-small.svelte b/svelte-app/src/components/icons/chevron-left-small.svelte new file mode 100644 index 000000000..3eaac08e3 --- /dev/null +++ b/svelte-app/src/components/icons/chevron-left-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-right-small.svelte b/svelte-app/src/components/icons/chevron-right-small.svelte new file mode 100644 index 000000000..140b0c5d4 --- /dev/null +++ b/svelte-app/src/components/icons/chevron-right-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/chevron-up-small.svelte b/svelte-app/src/components/icons/chevron-up-small.svelte new file mode 100644 index 000000000..b5c93d708 --- /dev/null +++ b/svelte-app/src/components/icons/chevron-up-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/clipboard-document-check.svelte b/svelte-app/src/components/icons/clipboard-document-check.svelte new file mode 100644 index 000000000..2b2ce989d --- /dev/null +++ b/svelte-app/src/components/icons/clipboard-document-check.svelte @@ -0,0 +1,17 @@ + + + + diff --git a/svelte-app/src/components/icons/clipboard-document.svelte b/svelte-app/src/components/icons/clipboard-document.svelte new file mode 100644 index 000000000..3a0bdfbab --- /dev/null +++ b/svelte-app/src/components/icons/clipboard-document.svelte @@ -0,0 +1,15 @@ + + + + diff --git a/svelte-app/src/components/icons/code-bracket.svelte b/svelte-app/src/components/icons/code-bracket.svelte new file mode 100644 index 000000000..8a67b5da8 --- /dev/null +++ b/svelte-app/src/components/icons/code-bracket.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/document-text-small.svelte b/svelte-app/src/components/icons/document-text-small.svelte new file mode 100644 index 000000000..c5dbabdfa --- /dev/null +++ b/svelte-app/src/components/icons/document-text-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/envelope-open-small.svelte b/svelte-app/src/components/icons/envelope-open-small.svelte new file mode 100644 index 000000000..a42560d5a --- /dev/null +++ b/svelte-app/src/components/icons/envelope-open-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/envelope-small.svelte b/svelte-app/src/components/icons/envelope-small.svelte new file mode 100644 index 000000000..d89770937 --- /dev/null +++ b/svelte-app/src/components/icons/envelope-small.svelte @@ -0,0 +1,13 @@ + + + + diff --git a/svelte-app/src/components/icons/exclamation-circle-small.svelte b/svelte-app/src/components/icons/exclamation-circle-small.svelte new file mode 100644 index 000000000..71050e0bb --- /dev/null +++ b/svelte-app/src/components/icons/exclamation-circle-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/exclamation-circle.svelte b/svelte-app/src/components/icons/exclamation-circle.svelte new file mode 100644 index 000000000..1d63d79a0 --- /dev/null +++ b/svelte-app/src/components/icons/exclamation-circle.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/globe-americas-small.svelte b/svelte-app/src/components/icons/globe-americas-small.svelte new file mode 100644 index 000000000..48be12cb1 --- /dev/null +++ b/svelte-app/src/components/icons/globe-americas-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/globe-asia-australia-small.svelte b/svelte-app/src/components/icons/globe-asia-australia-small.svelte new file mode 100644 index 000000000..3fba9edc1 --- /dev/null +++ b/svelte-app/src/components/icons/globe-asia-australia-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/language-small.svelte b/svelte-app/src/components/icons/language-small.svelte new file mode 100644 index 000000000..324d17374 --- /dev/null +++ b/svelte-app/src/components/icons/language-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/minus-circle-small.svelte b/svelte-app/src/components/icons/minus-circle-small.svelte new file mode 100644 index 000000000..5316ab002 --- /dev/null +++ b/svelte-app/src/components/icons/minus-circle-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/minus-small.svelte b/svelte-app/src/components/icons/minus-small.svelte new file mode 100644 index 000000000..0fcaff89d --- /dev/null +++ b/svelte-app/src/components/icons/minus-small.svelte @@ -0,0 +1,8 @@ + + + diff --git a/svelte-app/src/components/icons/moon-small.svelte b/svelte-app/src/components/icons/moon-small.svelte new file mode 100644 index 000000000..123891b19 --- /dev/null +++ b/svelte-app/src/components/icons/moon-small.svelte @@ -0,0 +1,10 @@ + + + diff --git a/svelte-app/src/components/icons/plus-circle-small.svelte b/svelte-app/src/components/icons/plus-circle-small.svelte new file mode 100644 index 000000000..de584e506 --- /dev/null +++ b/svelte-app/src/components/icons/plus-circle-small.svelte @@ -0,0 +1,12 @@ + + + diff --git a/svelte-app/src/components/icons/plus-small.svelte b/svelte-app/src/components/icons/plus-small.svelte new file mode 100644 index 000000000..2491b7b90 --- /dev/null +++ b/svelte-app/src/components/icons/plus-small.svelte @@ -0,0 +1,10 @@ + + + diff --git a/svelte-app/src/components/icons/sun-small.svelte b/svelte-app/src/components/icons/sun-small.svelte new file mode 100644 index 000000000..96d59e0e5 --- /dev/null +++ b/svelte-app/src/components/icons/sun-small.svelte @@ -0,0 +1,10 @@ + + + diff --git a/svelte-app/src/components/images/image-modal.svelte b/svelte-app/src/components/images/image-modal.svelte index 28a5bdcc5..bb9a9fbab 100644 --- a/svelte-app/src/components/images/image-modal.svelte +++ b/svelte-app/src/components/images/image-modal.svelte @@ -1,27 +1,35 @@ - { - if (show) { - switch (e.key) { - case 'Escape': - show = false; - break; - case 'Tab': - e.preventDefault(); - dialog.focus(); - break; + let closeButton: HTMLSpanElement; + + const onKeyUp = (e: KeyboardEvent) => { + if (!show) { + return; + } + if (e.key === ' ' || e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + show = false; + } + }, + onKeyDown = (e: KeyboardEvent) => { + if (!show || e.key !== 'Tab') { + return; } - } - }} -/> + e.stopPropagation(); + e.preventDefault(); + closeButton?.focus(); + }; + + + {#if show} @@ -29,14 +37,29 @@ class="fixed inset-0 z-50 flex h-full w-full cursor-zoom-out flex-col items-center justify-center bg-black/80" bind:this={dialog} on:click={() => (show = false)} - on:keydown={(e) => { - if (e.key === 'Escape') { - show = false; - } - }} + on:keydown={onKeyDown} + on:keyup={onKeyUp} in:fade={{ duration: BASE_ANIMATION_DURATION }} out:fade={{ duration: BASE_ANIMATION_DURATION }} > + { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + e.preventDefault(); + show = false; + } + }} + bind:this={closeButton} + > + {$t('Close')} +
diff --git a/svelte-app/src/components/images/image.svelte b/svelte-app/src/components/images/image.svelte index 16eb3fd8b..49d147b9c 100644 --- a/svelte-app/src/components/images/image.svelte +++ b/svelte-app/src/components/images/image.svelte @@ -3,6 +3,7 @@ import { crossfade, fade } from 'svelte/transition'; import { BASE_ANIMATION_DURATION } from '$lib/consts'; + import { t } from '$lib/i18n'; import { buildImageUrl, getCrop } from '$lib/sanity'; import ImageModal from '$components/images/image-modal.svelte'; @@ -65,38 +66,41 @@ }); -
+
{#await srcPromise || new Promise((_res) => {})}
{:then src} - + {#if dir === 'left'} + + + + + {:else} + + + + + {/if} + + diff --git a/svelte-app/src/components/portable-text/portable-text.svelte b/svelte-app/src/components/portable-text/portable-text.svelte index 3f4dd67ab..b373a8d4f 100644 --- a/svelte-app/src/components/portable-text/portable-text.svelte +++ b/svelte-app/src/components/portable-text/portable-text.svelte @@ -2,6 +2,8 @@ import { t } from '$lib/i18n'; import Logger from '$lib/logger'; + import ChevronDoubleUpSmall from '$components/icons/chevron-double-up-small.svelte'; + import ChevronUpSmall from '$components/icons/chevron-up-small.svelte'; import Footnote from '$components/portable-text/footnote.svelte'; import CodeBlock from '$components/portable-text/serializers/code-block.svelte'; import CustomCode from '$components/portable-text/serializers/custom-code.svelte'; @@ -16,6 +18,7 @@ import NullMark from '$components/portable-text/serializers/null-mark.svelte'; import OlWrapper from '$components/portable-text/serializers/ol-wrapper.svelte'; import UlWrapper from '$components/portable-text/serializers/ul-wrapper.svelte'; + import Tooltip from '$components/tooltips/tooltip.svelte'; import { PortableText } from '@portabletext/svelte'; @@ -144,18 +147,27 @@
  • - customScrollTo(e, `src-${note._key}`)} - on:keydown={(e) => { - if (e.code === 'Space' || e.code === 'Enter') { - customScrollTo(e, `src-${note._key}`); - } - }}>↑ + + customScrollTo(e, `src-${note._key}`)} + on:keydown={(e) => { + if (e.code === 'Space' || e.code === 'Enter') { + customScrollTo(e, `src-${note._key}`); + } + }} + > + + + + + +
  • {/each} diff --git a/svelte-app/src/components/portable-text/serializers/custom-heading.svelte b/svelte-app/src/components/portable-text/serializers/custom-heading.svelte index ea96027e6..34c9f39cf 100644 --- a/svelte-app/src/components/portable-text/serializers/custom-heading.svelte +++ b/svelte-app/src/components/portable-text/serializers/custom-heading.svelte @@ -1,29 +1,47 @@ - + diff --git a/svelte-app/src/components/portable-text/serializers/image.svelte b/svelte-app/src/components/portable-text/serializers/image.svelte index 0eb3da22f..6c4f24e26 100644 --- a/svelte-app/src/components/portable-text/serializers/image.svelte +++ b/svelte-app/src/components/portable-text/serializers/image.svelte @@ -13,5 +13,5 @@ - + diff --git a/svelte-app/src/components/sidebar.svelte b/svelte-app/src/components/sidebar.svelte new file mode 100644 index 000000000..9a7b9993c --- /dev/null +++ b/svelte-app/src/components/sidebar.svelte @@ -0,0 +1,152 @@ + + +
    + +
    + {#if config.image} + + {/if} +
    +

    + {name} +

    + {#if config.handle} +

    + {config.handle} +

    + {/if} +
    +
    + + {#if config.bio && socials} +
    + {#if config.bio} +

    + {config.bio} +

    + {/if} + {#if socials} +
      + {#each socials as social} +
    • + {#if social.url.includes('mailto')} + +
    • + {/each} +
    + {/if} +
    + {/if} + + +
    + + + + + + + + + {#if config.enableToru} + + {/if} + + {#if APP_VERSION?.length} + + {/if} +
    diff --git a/svelte-app/src/components/sidebar/profile-image.svelte b/svelte-app/src/components/sidebar/profile-image.svelte new file mode 100644 index 000000000..0ba4bfc87 --- /dev/null +++ b/svelte-app/src/components/sidebar/profile-image.svelte @@ -0,0 +1,25 @@ + + +kio.dev diff --git a/svelte-app/src/components/sidebar/sidebar-block.svelte b/svelte-app/src/components/sidebar/sidebar-block.svelte new file mode 100644 index 000000000..da302b1b1 --- /dev/null +++ b/svelte-app/src/components/sidebar/sidebar-block.svelte @@ -0,0 +1,135 @@ + + +{#if $isDesktop && $sidebarBlock} +
    + +

    + {$t('Reading')} — {estRemainingTime} {$t( + estRemainingTime === 1 ? 'min left' : 'mins left' + )} +

    + +

    + {title} +

    + + {#if tags?.length} +
    + {#each tags as tag} + + # + {tag.title.toLowerCase()} + + {/each} +
    + {/if} + + {#if desc} +

    + {desc} +

    + {/if} + + {#if $sidebarHeadings} +
    + {$t('Summary')} +
    +
    + +
    + {/if} +
    +
    +{/if} diff --git a/svelte-app/src/components/sidebar/sidebar-headings.svelte b/svelte-app/src/components/sidebar/sidebar-headings.svelte new file mode 100644 index 000000000..844ac9082 --- /dev/null +++ b/svelte-app/src/components/sidebar/sidebar-headings.svelte @@ -0,0 +1,35 @@ + + +{#each headings as heading} + +{/each} diff --git a/svelte-app/src/components/sidebar/sidebar-link.svelte b/svelte-app/src/components/sidebar/sidebar-link.svelte new file mode 100644 index 000000000..38a438c2c --- /dev/null +++ b/svelte-app/src/components/sidebar/sidebar-link.svelte @@ -0,0 +1,91 @@ + + + + e.key === 'Enter' && handleAction(e)} +> + {$t(link.name)} + + + + + diff --git a/svelte-app/src/components/sidebar/sidebar-tooltip.svelte b/svelte-app/src/components/sidebar/sidebar-tooltip.svelte new file mode 100644 index 000000000..76c221a82 --- /dev/null +++ b/svelte-app/src/components/sidebar/sidebar-tooltip.svelte @@ -0,0 +1,42 @@ + + +
    + +

    {title}

    +
    +

    {description}

    +
    diff --git a/svelte-app/src/components/sidebar/tooltips.ts b/svelte-app/src/components/sidebar/tooltips.ts new file mode 100644 index 000000000..b1d64b03a --- /dev/null +++ b/svelte-app/src/components/sidebar/tooltips.ts @@ -0,0 +1,3 @@ +import { writable } from 'svelte/store'; + +export const tooltipContent = writable({}); diff --git a/svelte-app/src/components/sidebar/toru.svelte b/svelte-app/src/components/sidebar/toru.svelte new file mode 100644 index 000000000..bf91e6fd3 --- /dev/null +++ b/svelte-app/src/components/sidebar/toru.svelte @@ -0,0 +1,149 @@ + + +{#if $data && $isDesktop} + +
    + Album art for the currently playing track + {#if playing} +
    +
    +
    + {#each Array(4) as _} + + {/each} +
    +
    + {/if} +
    + +
    +

    + {$t(playing ? 'toru.status.playing' : 'toru.status.paused')} +

    +
    +

    + {title ?? 'Unknown title'} +

    + +

    + {artist ?? ''} + {artist && album ? '—' : ''} + {album ?? ''} +

    +
    +
    + + + + + +
    +{/if} + + diff --git a/svelte-app/src/components/experiments/toru.ts b/svelte-app/src/components/sidebar/toru.ts similarity index 65% rename from svelte-app/src/components/experiments/toru.ts rename to svelte-app/src/components/sidebar/toru.ts index c573aaa21..7ee95039b 100644 --- a/svelte-app/src/components/experiments/toru.ts +++ b/svelte-app/src/components/sidebar/toru.ts @@ -1,3 +1,5 @@ +import { writable } from 'svelte/store'; + import { browser } from '$app/environment'; import Logger from '$lib/logger'; @@ -20,6 +22,8 @@ let socketInstance: WebSocket | undefined, const interval = 36_000; +export const data = writable(undefined); + const onOpen = () => { Logger.info('[ToruSync] Connected'); retries = 0; @@ -27,33 +31,32 @@ const onOpen = () => { const onClose = () => { Logger.info('[ToruSync] Disconnected'); - stop(); + // stop(); }; const onError = (e: Event) => { Logger.error('[ToruSync] Error', e); }; -const onMessage = - (onUpdate: (data: ToruData) => void) => (e: MessageEvent) => { - if (!e.data || e.data === 'pong') { - return; - } +const onMessage = (e: MessageEvent) => { + if (!e.data || e.data === 'pong') { + return; + } - try { - const res = JSON.parse(e.data as string) as ToruData; + try { + const res = JSON.parse(e.data as string) as ToruData; - Logger.info('[ToruSync] Received frame'); + Logger.info('[ToruSync] Received frame'); - if (res.title || res.album || res.artist) { - onUpdate(res); - } - } catch (e) { - Logger.error('[ToruSync] Error parsing', e); + if (res.title || res.album || res.artist) { + data.set(res); } - }; + } catch (e) { + Logger.error('[ToruSync] Error parsing', e); + } +}; -export const initSync = (onUpdate: (data: ToruData) => void) => { +export const initSync = () => { if (!browser || (socketInstance && socketInstance.readyState === WebSocket.OPEN)) { return; } @@ -63,7 +66,7 @@ export const initSync = (onUpdate: (data: ToruData) => void) => { socketInstance = new WebSocket('wss://toru.kio.dev/api/v1/ws/kiosion?cover_size=large'); socketInstance.addEventListener('open', onOpen); - socketInstance.addEventListener('message', onMessage(onUpdate)); + socketInstance.addEventListener('message', onMessage); socketInstance.addEventListener('error', onError); socketInstance.addEventListener('close', onClose); @@ -74,19 +77,22 @@ export const initSync = (onUpdate: (data: ToruData) => void) => { }, interval); }; -export const stopSync = (onUpdate: (data: ToruData) => void) => { +export const stopSync = () => { if (!browser || !socketInstance) { return; } clearInterval(repeat); - if (socketInstance.readyState !== WebSocket.CLOSED) { + if ( + socketInstance.readyState === WebSocket.OPEN || + socketInstance.readyState === WebSocket.CONNECTING + ) { socketInstance.close(); } socketInstance.removeEventListener('open', onOpen); - socketInstance.removeEventListener('message', onMessage(onUpdate)); + socketInstance.removeEventListener('message', onMessage); socketInstance.removeEventListener('error', onError); socketInstance.removeEventListener('close', onClose); diff --git a/svelte-app/src/components/tooltips/inner.svelte b/svelte-app/src/components/tooltips/inner.svelte index 2b9189b54..ce31f3261 100644 --- a/svelte-app/src/components/tooltips/inner.svelte +++ b/svelte-app/src/components/tooltips/inner.svelte @@ -1,144 +1,121 @@ diff --git a/svelte-app/src/components/tooltips/manager.svelte b/svelte-app/src/components/tooltips/manager.svelte deleted file mode 100644 index 390f45760..000000000 --- a/svelte-app/src/components/tooltips/manager.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -{#each $tooltips as tooltip (tooltip.id)} - -{/each} diff --git a/svelte-app/src/components/tooltips/tooltip.svelte b/svelte-app/src/components/tooltips/tooltip.svelte index 2dbf1b0fd..c9b3ff783 100644 --- a/svelte-app/src/components/tooltips/tooltip.svelte +++ b/svelte-app/src/components/tooltips/tooltip.svelte @@ -1,51 +1,47 @@ + +{#if active && target} + fade(node, { ...(args ?? {}), easing: cubicInOut }) + : (node, args) => fly(node, { ...(args ?? {}), easing: cubicInOut, x: 4 })} + {duration} + {placement} + {offset} + {target} + {showArrow} + {followCursor} + className={content && + 'bg-neutral-600 font-mono text-xs text-light dark:bg-neutral-100 dark:text-dark whitespace-nowrap rounded-md'} + > + {#if content} + {content} + {:else} + + {/if} + +{/if} diff --git a/svelte-app/src/languages/en.json b/svelte-app/src/languages/en.json index 052b8c353..1cf315c3e 100644 --- a/svelte-app/src/languages/en.json +++ b/svelte-app/src/languages/en.json @@ -11,9 +11,12 @@ "Click here to": "Click here to", "Code block": "Code block", "Code content": "Code content", + "Contents": "Contents", "Copied": "Copied", "Copy": "Copy", "Copy to clipboard": "Copy to clipboard", + "Copy {value}": "Copy {value}", + "Dark": "Dark", "Date posted": "Date posted", "Duration": "Duration", "English": "English", @@ -22,44 +25,48 @@ "Experiments": "Experiments", "Footnotes": "Footnotes", "French": "French", + "Go back": "Go back", "Go to footnote": "Go to footnote", "Go to footnote source": "Go to footnote source", - "Hide stack trace": "Hide stack trace", "Home": "Home", "Internal Error": "Internal Error", "Invalid date": "Invalid date", "Language": "Language", - "Lights off": "Lights off", - "Lights on": "Lights on", + "Light": "Light", "Links": "Links", "Meta": "Meta", "More": "More", "No content": "No content", "PGP": "PGP", "Page content": "Page content", + "Pages": "Pages", "Please": "Please", "Post": "Post", "Posts": "Posts", "Project": "Project", "Projects": "Projects", "Read more": "Read more", + "Reading": "Reading", "Recent projects": "Recent projects", "Recent thoughts": "Recent thoughts", + "Reload page": "Reload page", "Say hello": "Say hello", "See more": "See more", - "Show stack trace": "Show stack trace", "Show {amount}": "Show {amount}", "Skip to content": "Skip to content", "Social": "Social", "Social links": "Social links", "Sorry, something went wrong. Please try again.": "Sorry, something went wrong. Please try again.", + "Summary": "Summary", "Switch to English": "Switch to English", "Switch to French": "Switch to French", + "Switch to {opt}": "Switch to {opt}", "Tags": "Tags", "Theme": "Theme", "Thoughts": "Thoughts", "Topic": "Topic", "Topics": "Topics", + "Uncategorized": "Uncategorized", "Unknown date": "Unknown date", "Use dark mode": "Use dark mode", "Use light mode": "Use light mode", @@ -98,8 +105,9 @@ "message": "No content." } }, - "go back": "go back", "less": "less", + "min left": "min left", + "mins left": "mins left", "more": "more", "now": "now", "pages": { @@ -130,8 +138,15 @@ }, "present": "present", "refresh the page": "refresh the page", + "toru": { + "status": { + "playing": "Listening to", + "paused": "Last listened to" + } + }, "{document} details": "{document} details", "{length} min read": "{length} min read", + "{length} words": "{length} words", "{months} months": "{months} months", "{month} month": "{month} month", "{stars} stars": "{stars} stars", diff --git a/svelte-app/src/languages/fr.json b/svelte-app/src/languages/fr.json index ece73181e..ef59ebb6e 100644 --- a/svelte-app/src/languages/fr.json +++ b/svelte-app/src/languages/fr.json @@ -11,9 +11,12 @@ "Click here to": "Cliquez ici pour", "Code block": "Bloc de code", "Code content": "Contenu du code", + "Contents": "Sommaire", "Copied": "Copié", "Copy": "Copier", "Copy to clipboard": "Copier dans le presse-papiers", + "Copy {value}": "Copier {value}", + "Dark": "Sombre", "Date posted": "Date de publication", "Duration": "Durée", "English": "Anglais", @@ -22,44 +25,48 @@ "Experiments": "Expériences", "Footnotes": "Notes de bas de page", "French": "Français", + "Go back": "Retour", "Go to footnote": "Aller à la note de bas de page", "Go to footnote source": "Aller à la source de la note de bas de page", - "Hide stack trace": "Masquer la trace de la pile", "Home": "Accueil", "Internal Error": "Erreur interne", "Invalid date": "Date invalide", "Language": "Langue", - "Lights off": "Lumières éteintes", - "Lights on": "Lumières allumées", + "Light": "Clair", "Links": "Liens", "Meta": "Plus", "More": "Plus", "No content": "Pas de contenu", "PGP": "PGP", "Page content": "Contenu de la page", + "Pages": "Pages", "Please": "S'il vous plaît", "Post": "Article", "Posts": "Articles", "Project": "Projet", "Projects": "Projets", "Read more": "Lire la suite", + "Reading": "Lecture", "Recent projects": "Projets récents", "Recent thoughts": "Pensées récentes", + "Reload page": "Recharger la page", "Say hello": "Dire bonjour", "See more": "Voir plus", - "Show stack trace": "Afficher la trace de la pile", "Show {amount}": "Afficher {amount}", "Skip to content": "Passer au contenu", "Social": "Social", "Social links": "Liens sociaux", "Sorry, something went wrong. Please try again.": "Désolé, quelque chose s'est mal passé. Veuillez réessayer.", + "Summary": "Résumé", "Switch to English": "Passer à l'Anglais", "Switch to French": "Passer au Français", + "Switch to {opt}": "Passer à {opt}", "Tags": "Tags", "Theme": "Thème", "Thoughts": "Pensées", "Topic": "Sujet", "Topics": "Sujets", + "Uncategorized": "Non classé", "Unknown date": "Date inconnue", "Use dark mode": "Utiliser le mode sombre", "Use light mode": "Utiliser le mode clair", @@ -98,8 +105,9 @@ "message": "Pas de contenu." } }, - "go back": "retourner", "less": "moins", + "min left": "min rest", + "mins left": "mins rest", "more": "plus", "now": "maintenant", "pages": { @@ -130,8 +138,15 @@ }, "present": "présent", "refresh the page": "rafraîchir la page", + "toru": { + "status": { + "playing": "Écoute de", + "paused": "Dernièrement écouté" + } + }, "{document} details": "Détails de {document}", "{length} min read": "{length} minute de lecture", + "{length} words": "{length} mots", "{months} months": "{months} mois", "{month} month": "{month} mois", "{stars} stars": "{stars} étoiles", diff --git a/svelte-app/src/lib/consts.ts b/svelte-app/src/lib/consts.ts index df8c4f526..dfdbbb37e 100644 --- a/svelte-app/src/lib/consts.ts +++ b/svelte-app/src/lib/consts.ts @@ -57,13 +57,17 @@ export const APP_ROUTES = [ path: '/etc', hidden: false } -] as AppRoute[]; +] as const satisfies AppRoute[]; export const TOP_LEVEL_ROUTES = APP_ROUTES.map((r) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { children, ...rest } = r; + if (!('children' in r)) { + return r; + } + + const { children: _children, ...rest } = r; + return rest; -}) as Omit[]; +}) satisfies Omit[]; export const ROUTE_ORDER = [ '/', @@ -92,8 +96,8 @@ export const ERRORS = { export const BASE_TRANSITION_DURATION = 200 as const; export const BASE_ANIMATION_DURATION = 300 as const; -export const HOMEPAGE_POSTS_NUM = 3 as const; -export const HOMEPAGE_PROJECTS_NUM = 3 as const; +export const HOMEPAGE_POSTS_NUM = 4 as const; +export const HOMEPAGE_PROJECTS_NUM = 4 as const; export const RECENT_POSTS_COUNT = 99; export const RECENT_PROJECTS_COUNT = 99; diff --git a/svelte-app/src/lib/i18n.ts b/svelte-app/src/lib/i18n.ts index b16e41e51..97001effc 100644 --- a/svelte-app/src/lib/i18n.ts +++ b/svelte-app/src/lib/i18n.ts @@ -90,9 +90,10 @@ const _translate = ( key: K, params?: ExtractTVars extends never ? never - : Record, string | number> + : Record, string | number>, + overrideLang?: string ): string => { - const lang = currentLang || DEFAULT_APP_LANG; + const lang = overrideLang || currentLang || DEFAULT_APP_LANG; // For any provided params, replace the corresponding placeholders if any // e.g. "Hello {name}" with { name: "World" } becomes "Hello World" @@ -123,7 +124,8 @@ const translate = derived< key: K, params?: ExtractTVars extends never ? never - : Record, string | number> + : Record, string | number>, + overrideLang?: string ) => string >( currentLang, @@ -132,9 +134,10 @@ const translate = derived< key: K, params?: ExtractTVars extends never ? never - : Record, string | number> + : Record, string | number>, + overrideLang?: string ) => - _translate(val, key, params) + _translate(val, key, params, overrideLang) ); const addSearchParams = (path: string, params?: URLSearchParams): string => { diff --git a/svelte-app/src/lib/route-trie.ts b/svelte-app/src/lib/route-trie.ts index d893ebc24..8c99274ec 100644 --- a/svelte-app/src/lib/route-trie.ts +++ b/svelte-app/src/lib/route-trie.ts @@ -23,7 +23,7 @@ export default class RouteTrie { } search(route: string): number | null { - let node = this.root; + const node = this.root; const parts = route.split('/').filter(Boolean); let bestMatchIndex: number | null = null; let bestMatchDepth = -1; @@ -33,7 +33,9 @@ export default class RouteTrie { bestMatchIndex = node.index; bestMatchDepth = depth; } - if (depth >= parts.length) return; + if (depth >= parts.length) { + return; + } const part = parts[depth]; if (node.children[part]) { diff --git a/svelte-app/src/lib/sanity.ts b/svelte-app/src/lib/sanity.ts index d32e2fc09..00fb872f4 100644 --- a/svelte-app/src/lib/sanity.ts +++ b/svelte-app/src/lib/sanity.ts @@ -5,6 +5,7 @@ import imageUrlBuilder from '@sanity/image-url'; import type { ImageUrlBuilder } from '@sanity/image-url/lib/types/builder'; import type { FitMode, + ImageFormat, SanityClientLike, SanityImageObject, SanityImageSource @@ -63,6 +64,7 @@ type baseBuildImageUrlOptions = { height?: number; blur?: number; fit?: FitMode; + format?: ImageFormat; }; type buildImageUrlOptions = baseBuildImageUrlOptions & @@ -78,7 +80,7 @@ type buildImageUrlOptions = baseBuildImageUrlOptions & ); export const buildImageUrl = ( - { baseUrl, ref, crop, width, height, blur, fit }: buildImageUrlOptions = { + { baseUrl, ref, crop, width, height, blur, fit, format }: buildImageUrlOptions = { baseUrl: undefined } as buildImageUrlOptions ) => { @@ -102,5 +104,10 @@ export const buildImageUrl = ( } else if (crop) { baseUrl = baseUrl.fit('crop'); } - return baseUrl.auto('format').url(); + if (format) { + baseUrl = baseUrl.format(format); + } else { + baseUrl = baseUrl.auto('format'); + } + return baseUrl.url(); }; diff --git a/svelte-app/src/lib/sidebar.ts b/svelte-app/src/lib/sidebar.ts new file mode 100644 index 000000000..fd2f3e168 --- /dev/null +++ b/svelte-app/src/lib/sidebar.ts @@ -0,0 +1,16 @@ +/* eslint-disable func-call-spacing */ +import { writable } from 'svelte/store'; + +import type { PostDocument, ProjectDocument } from '$types'; + +type SidebarBlockContent = Pick< + PostDocument | ProjectDocument, + 'title' | 'desc' | 'tags' | 'date' | 'estimatedReadingTime' | 'views' + // eslint-disable-next-line @typescript-eslint/ban-types +> & {}; + +export const sidebarBlock = writable(undefined); + +export const sidebarHeadings = writable< + (PostDocument | ProjectDocument)['headings'] | undefined +>(undefined); diff --git a/svelte-app/src/lib/tooltips.ts b/svelte-app/src/lib/tooltips.ts deleted file mode 100644 index 3b05eecad..000000000 --- a/svelte-app/src/lib/tooltips.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { writable } from 'svelte/store'; - -type BasicPlacement = 'top' | 'bottom' | 'left' | 'right'; - -type Placement = BasicPlacement | `${BasicPlacement}-start` | `${BasicPlacement}-end`; - -export type Tooltip = { - id: number; - content: string; - duration: number; - placement: Placement; - followCursor: boolean; - target: HTMLElement; - offset: [number, number]; - delay: number; -}; - -const tooltips = writable([]); - -const createTooltip = (data: Tooltip) => { - tooltips.update((t) => { - const existing = t.findIndex( - (tooltip) => tooltip.id === data.id || tooltip.target === data.target - ); - if (existing !== -1) { - t.splice(existing, 1); - } - t.push(data); - return t; - }); - return data.id; -}; - -const updateTooltip = ( - id: number, - data: Partial> -) => { - tooltips.update((t) => { - const existing = t.findIndex((tooltip) => tooltip.id === id); - if (existing !== -1) { - t[existing] = { ...t[existing], ...data }; - } - return t; - }); -}; - -const destroyTooltip = (id: number) => { - tooltips.update((t) => { - const filtered = []; - - for (let i = 0; i < t.length; ++i) { - if (t[i].id !== id) { - filtered.push(t[i]); - } - } - - return filtered; - }); -}; - -export { createTooltip, destroyTooltip, tooltips, updateTooltip }; diff --git a/svelte-app/src/lib/utils.ts b/svelte-app/src/lib/utils.ts index c08c206cb..6ed02f160 100644 --- a/svelte-app/src/lib/utils.ts +++ b/svelte-app/src/lib/utils.ts @@ -1,10 +1,10 @@ // Misc utils that don't fit anywhere else import { derived, get } from 'svelte/store'; +import { ROUTE_ORDER } from '$lib/consts'; import { currentLang, isLocalized } from '$lib/i18n'; import Logger from '$lib/logger'; import RouteTrie from '$lib/route-trie'; -import { ROUTE_ORDER } from '$lib/consts'; const _parseViews = (views: number | undefined, lang: string) => { if (!views || views < 1) { diff --git a/svelte-app/src/routes/+error.svelte b/svelte-app/src/routes/+error.svelte index b9ba25635..4dcc84bcc 100644 --- a/svelte-app/src/routes/+error.svelte +++ b/svelte-app/src/routes/+error.svelte @@ -1,14 +1,17 @@ + kio.dev | {status} -
    - -

    +

    + + + window.history.length > 2 ? window.history.back() : goto($linkTo('/'))} + /> + + + + +

    + {heading} +

    +

    {$page.error?.message && $page.status !== 404 ? $page.error.message : $t(message)}

    -

    - {$t('Please')} - window.history.back()}>{$t('go back')}, or window.location.reload()}>{$t('refresh the page')}. -

    - - - {#if causes?.length} -
    - - (showStack = !showStack)} - /> -
    - {#if showStack} -
    -
    -
    {#each causes as cause, i}{cause?.trim?.()}{#if i < causes.length - 1}
    {/if}{/each}
    -
    -
    + type="button" + > + {#if showStack} + +
    + + {#if showStack} +
    +
    {#each causes as cause, i}{cause?.trim?.()}{#if i < causes.length - 1}
    {/if}{/each}
    +
    {/if}
    diff --git a/svelte-app/src/routes/+layout.svelte b/svelte-app/src/routes/+layout.svelte index be6291695..f7a7fff40 100644 --- a/svelte-app/src/routes/+layout.svelte +++ b/svelte-app/src/routes/+layout.svelte @@ -21,18 +21,15 @@ import { check as checkTranslations, currentLang, isLocalized, t } from '$lib/i18n'; import Settings, { listenForMQLChange, loading } from '$lib/settings'; - import Footer from '$components/footer.svelte'; - import Header from '$components/header.svelte'; import PageTransition from '$components/layouts/page-transition.svelte'; - import ScrollContainer from '$components/layouts/scroll-container.svelte'; - import TooltipManager from '$components/tooltips/manager.svelte'; + import Sidebar from '$components/sidebar.svelte'; import type { Unsubscriber } from 'svelte/store'; let unsubscribers = [] as Unsubscriber[], HighlightStyles: string | undefined, setLoadingTimer: ReturnType | undefined, - scrollShadow = { top: false, bottom: false }; + scrollContainer: HTMLElement | null; const { theme } = Settings, skipToContent = (e: KeyboardEvent) => { @@ -149,22 +146,20 @@ on:keydown={skipToContent}>{$t('Skip to content')} -
    - - -
    - - - - - - - -
    - - - - +
    +
    + + +
    + + + +
    +
    diff --git a/svelte-app/src/routes/+layout.ts b/svelte-app/src/routes/+layout.ts index c03291472..dccf4cb45 100644 --- a/svelte-app/src/routes/+layout.ts +++ b/svelte-app/src/routes/+layout.ts @@ -1,4 +1,4 @@ -import { DEFAULT_APP_LANG } from '$lib/consts'; +import { DEFAULT_APP_LANG, TORU_API_URL } from '$lib/consts'; import { ENV } from '$lib/env'; import Logger from '$lib/logger'; import { findOne } from '$lib/store'; @@ -6,6 +6,7 @@ import { findOne } from '$lib/store'; import { error } from '@sveltejs/kit'; import type { LayoutLoad } from './$types'; +import type { ToruData } from '$components/sidebar/toru'; import type { SiteConfig } from '$types'; export const trailingSlash = 'ignore'; @@ -19,13 +20,38 @@ export const load = (async ({ params, url, fetch }) => { Logger.error('Failed to load layout data:', e); throw error(500, { message: 'Sorry, something went wrong during load.', + // @ts-expect-error - Overriding base type cause: e?.cause, stack: e?.stack }); })) as SiteConfig; + const toruData = ( + config.enableToru + ? (fetch(`${TORU_API_URL}/kiosion?res=json&cover_size=medium`) + .then((res) => { + if (!res.ok) { + throw new Error('Failed to fetch now playing data'); + } + + return res + .json() + .then((data) => data.data) + .catch((e) => { + throw new Error('Failed to parse now playing data', e); + }); + }) + .catch((e) => { + Logger.error(e); + + return undefined; + }) as Promise) + : Promise.resolve(undefined) + ) satisfies Promise; + return { pathname: url.pathname, + toruData, config }; }) satisfies LayoutLoad; diff --git a/svelte-app/src/routes/[[lang=lang]]/+page.svelte b/svelte-app/src/routes/[[lang=lang]]/+page.svelte index b0e96b17b..562fbe227 100644 --- a/svelte-app/src/routes/[[lang=lang]]/+page.svelte +++ b/svelte-app/src/routes/[[lang=lang]]/+page.svelte @@ -1,12 +1,11 @@ @@ -26,14 +24,20 @@ - - + + + - + - + - + - - + + + diff --git a/svelte-app/src/routes/[[lang=lang]]/experiments/+page.ts b/svelte-app/src/routes/[[lang=lang]]/experiments/+page.ts deleted file mode 100644 index 53dc5a2ee..000000000 --- a/svelte-app/src/routes/[[lang=lang]]/experiments/+page.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { TORU_API_URL } from '$lib/consts'; -import Logger from '$lib/logger'; - -import type { PageLoad } from './$types'; -import type { ToruData } from '$components/experiments/toru'; - -export const load = (({ fetch }) => { - const nowPlayingData = fetch(`${TORU_API_URL}/kiosion?res=json&cover_size=medium`) - .then((res) => { - if (!res.ok) { - throw new Error('Failed to fetch now playing data'); - } - - return res - .json() - .then((data) => data.data) - .catch((e) => { - throw new Error('Failed to parse now playing data', e); - }); - }) - .catch((e) => { - Logger.error(e); - - return undefined; - }) satisfies Promise; - - return { fetch, nowPlayingData }; -}) satisfies PageLoad; diff --git a/svelte-app/src/routes/[[lang=lang]]/thoughts/+/+page.svelte b/svelte-app/src/routes/[[lang=lang]]/thoughts/+/+page.svelte index 8204b0aef..0352ce762 100644 --- a/svelte-app/src/routes/[[lang=lang]]/thoughts/+/+page.svelte +++ b/svelte-app/src/routes/[[lang=lang]]/thoughts/+/+page.svelte @@ -4,80 +4,59 @@ import ArrowButton from '$components/controls/arrow-button.svelte'; import EmptyContent from '$components/empty-content.svelte'; import HeadedBlock from '$components/headings/headed-block.svelte'; - import Hoverable from '$components/hoverable.svelte'; + import BaseContainer from '$components/layouts/base-container.svelte'; export let data; - - -
    -

    - {$t('Topics')} -

    - -
    -
    +
    + +

    + {data.tags?.length ?? 0} + {$t('Topics').toLowerCase()} +

    + +
    - {#if data.tags?.length} - - {:else} - - {/if} - - - +

    + {tag.title} +

    + + + {posts?.length ?? 0} + {$t((posts?.length ?? 0) === 1 ? 'Post' : 'Posts').toLowerCase()} + + + {/each} +
    + {:else} + + {/if} +
    + +
    diff --git a/svelte-app/src/routes/[[lang=lang]]/thoughts/+/[slug]/+page.svelte b/svelte-app/src/routes/[[lang=lang]]/thoughts/+/[slug]/+page.svelte index 99a75b958..23efb5741 100644 --- a/svelte-app/src/routes/[[lang=lang]]/thoughts/+/[slug]/+page.svelte +++ b/svelte-app/src/routes/[[lang=lang]]/thoughts/+/[slug]/+page.svelte @@ -3,34 +3,36 @@ import ArrowButton from '$components/controls/arrow-button.svelte'; import EmptyContent from '$components/empty-content.svelte'; - import HeadedBlock from '$components/headings/headed-block.svelte'; + import BaseContainer from '$components/layouts/base-container.svelte'; import DocumentList from '$components/lists/document-list.svelte'; export let data; - - -
    -

    - {$t('Topic').toLowerCase()}: - {data.tag.title} -

    - +
    + +
    + # + {data.tag.title.toLowerCase()}
    - - {#if data.posts?.length} - - {:else} - - {/if} - + +
    + + + {#if data.posts?.length} + + {:else} + + {/if} + +
    diff --git a/svelte-app/src/routes/[[lang=lang]]/thoughts/+page.svelte b/svelte-app/src/routes/[[lang=lang]]/thoughts/+page.svelte index d97a2df2d..17d9fabec 100644 --- a/svelte-app/src/routes/[[lang=lang]]/thoughts/+page.svelte +++ b/svelte-app/src/routes/[[lang=lang]]/thoughts/+page.svelte @@ -4,7 +4,7 @@ import ArrowButton from '$components/controls/arrow-button.svelte'; import EmptyContent from '$components/empty-content.svelte'; - import HeadedBlock from '$components/headings/headed-block.svelte'; + import BaseContainer from '$components/layouts/base-container.svelte'; import DocumentList from '$components/lists/document-list.svelte'; import type { DocumentTags } from '$types'; @@ -13,7 +13,7 @@ $: description = $t('pages.thoughts.description'); - const MAX_TAGS = 6; + const MAX_TAGS = 5; const tags: DocumentTags[] = [], tagCounts: Record = {}; @@ -68,54 +68,44 @@ - - {#if data.posts.length} - {#if tags.length} -
    - ( - - {#each tags as tag, i} - {#if i < MAX_TAGS} - - - {tag.title.toLowerCase()}{#if i < MAX_TAGS && i < tags.length - 1},{/if} + + + + + + {#if data.posts.length} + + {:else} + {/if} - - - {:else} - - {/if} - + +
    diff --git a/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.svelte b/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.svelte index 701e5ae4f..01057f350 100644 --- a/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.svelte +++ b/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.svelte @@ -4,4 +4,4 @@ export let data; - + diff --git a/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.ts b/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.ts index 133651394..d16119e15 100644 --- a/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.ts +++ b/svelte-app/src/routes/[[lang=lang]]/thoughts/[slug]/+page.ts @@ -16,7 +16,9 @@ export const load = (async ({ parent, fetch, params, url }) => { const post = // in some cases _parent isn't defined during SSR...? - (!preview && _parent?.posts?.find?.((post) => post.slug?.current === params.slug)) || + (!preview && + opts.lang === DEFAULT_APP_LANG && + _parent?.posts?.find?.((post) => post.slug?.current === params.slug)) || handleLoadError(await findOne(fetch, 'post', opts)); post && incViews(fetch, post); diff --git a/svelte-app/src/routes/[[lang=lang]]/work/+page.svelte b/svelte-app/src/routes/[[lang=lang]]/work/+page.svelte index 11a011c57..42dee818a 100644 --- a/svelte-app/src/routes/[[lang=lang]]/work/+page.svelte +++ b/svelte-app/src/routes/[[lang=lang]]/work/+page.svelte @@ -3,10 +3,10 @@ import { pageTitle } from '$lib/navigation'; import Timeline from '$components/about/timeline.svelte'; - import Divider from '$components/divider.svelte'; import EmptyContent from '$components/empty-content.svelte'; import HeadedBlock from '$components/headings/headed-block.svelte'; - import DocumentList from '$components/lists/document-list.svelte'; + import BaseContainer from '$components/layouts/base-container.svelte'; + import ListItem from '$components/lists/list-item.svelte'; export let data; @@ -26,20 +26,32 @@ - - {#if data.config?.timeline?.length} - - {:else} -
    - -
    - {/if} -
    - -{#if data.projects.length} - +
    + + + {#if data.config?.timeline?.length} + + {:else} +
    + +
    + {/if} +
    +
    - - - -{/if} + {#if data.projects.length} + + +
    + {#each data.projects as project} + + {/each} +
    +
    +
    + {/if} +
    diff --git a/svelte-app/src/routes/[[lang=lang]]/work/[slug]/+page.ts b/svelte-app/src/routes/[[lang=lang]]/work/[slug]/+page.ts index 14e471bb3..d9bc2b43c 100644 --- a/svelte-app/src/routes/[[lang=lang]]/work/[slug]/+page.ts +++ b/svelte-app/src/routes/[[lang=lang]]/work/[slug]/+page.ts @@ -37,6 +37,7 @@ export const load = (async ({ parent, fetch, params, url }) => { >, project = (!preview && + opts.lang === DEFAULT_APP_LANG && _parent?.projects?.find?.((proj) => proj.slug?.current === params.slug)) || handleLoadError(await findOne(fetch, 'project', opts)); diff --git a/svelte-app/src/styles/_tw.scss b/svelte-app/src/styles/_tw.scss index c4564ad36..2c5404ae5 100644 --- a/svelte-app/src/styles/_tw.scss +++ b/svelte-app/src/styles/_tw.scss @@ -29,7 +29,7 @@ @apply bg-neutral-800 text-light; &.light { - @apply bg-neutral-100 text-dark; + @apply bg-neutral-0 text-dark; } &:not(.is-loaded) { diff --git a/svelte-app/tests/routes/error.test.ts b/svelte-app/tests/routes/error.test.ts index ab1d12967..ba7c68c1c 100644 --- a/svelte-app/tests/routes/error.test.ts +++ b/svelte-app/tests/routes/error.test.ts @@ -24,7 +24,7 @@ test('handles config load error', async ({ context, page }) => { timeout: 4000 }); - await page.click('text=See more'); + await page.click('text=Show more'); expect(await page.textContent('[data-test-id="error-page"]')).toContain( 'Failed to fetch config data.' @@ -46,7 +46,7 @@ test('handles project load error', async ({ context, page }) => { timeout: 4000 }); - await page.click('text=See more'); + await page.click('text=Show more'); expect(await page.textContent('[data-test-id="error-page"]')).toContain( '404: Not Found' @@ -71,7 +71,7 @@ test('handles post load error', async ({ context, page }) => { timeout: 4000 }); - await page.click('text=See more'); + await page.click('text=Show more'); expect(await page.textContent('[data-test-id="error-page"]')).toContain( '404: Not Found' diff --git a/svelte-app/tests/routes/index.test.ts b/svelte-app/tests/routes/index.test.ts index 58f8c1b94..d92762f3d 100644 --- a/svelte-app/tests/routes/index.test.ts +++ b/svelte-app/tests/routes/index.test.ts @@ -48,7 +48,7 @@ test('should render error page on failed data fetch', async ({ context, page }) await page.waitForSelector('body.is-loaded'); expect(await page.waitForSelector('[data-test-id="error-page"]')).toBeTruthy(); - await page.click('text=See more'); + await page.click('text=Show more'); expect(await page.textContent('[data-test-id="error-page"]')).toContain( 'Failed to fetch config data.' ); diff --git a/svelte-app/tsconfig.json b/svelte-app/tsconfig.json index 4694e4772..94f3b0412 100644 --- a/svelte-app/tsconfig.json +++ b/svelte-app/tsconfig.json @@ -12,6 +12,7 @@ "noEmit": true, "removeComments": true, "rootDir": "./", + "moduleResolution": "node", "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, @@ -27,7 +28,5 @@ "*.js", "*.ts" ], - "exclude": [ - "**/node_modules" - ] + "exclude": ["**/node_modules"] } diff --git a/svelte-app/types/app/config/index.ts b/svelte-app/types/app/config/index.ts index 964cb9ba6..e45dfccc3 100644 --- a/svelte-app/types/app/config/index.ts +++ b/svelte-app/types/app/config/index.ts @@ -1,4 +1,9 @@ -import type { ArbitraryTypedObject, PortableTextBlock, SanityAsset } from '$types/sanity'; +import type { + ArbitraryTypedObject, + PortableTextBlock, + SanityAsset, + SanityImageObject +} from '$types/sanity'; interface ContentSection extends SanityAsset { title: string; @@ -20,6 +25,14 @@ export type WorkTimelineItem = SanityAsset & { }; export interface SiteConfig extends SanityAsset { + name: string; + image: SanityAsset & { + dark: SanityImageObject; + light: SanityImageObject; + }; + handle?: string; + bio?: string; + enableToru?: boolean; about: ContentSection[]; meta: ContentSection[]; timeline: WorkTimelineItem[]; diff --git a/svelte-app/types/generated/index.ts b/svelte-app/types/generated/index.ts index a419a0b9e..2aff9aa23 100644 --- a/svelte-app/types/generated/index.ts +++ b/svelte-app/types/generated/index.ts @@ -11,9 +11,12 @@ export type LocaleKey = | 'Click here to' | 'Code block' | 'Code content' + | 'Contents' | 'Copied' | 'Copy' | 'Copy to clipboard' + | 'Copy {value}' + | 'Dark' | 'Date posted' | 'Duration' | 'English' @@ -22,44 +25,48 @@ export type LocaleKey = | 'Experiments' | 'Footnotes' | 'French' + | 'Go back' | 'Go to footnote' | 'Go to footnote source' - | 'Hide stack trace' | 'Home' | 'Internal Error' | 'Invalid date' | 'Language' - | 'Lights off' - | 'Lights on' + | 'Light' | 'Links' | 'Meta' | 'More' | 'No content' | 'PGP' | 'Page content' + | 'Pages' | 'Please' | 'Post' | 'Posts' | 'Project' | 'Projects' | 'Read more' + | 'Reading' | 'Recent projects' | 'Recent thoughts' + | 'Reload page' | 'Say hello' | 'See more' - | 'Show stack trace' | 'Show {amount}' | 'Skip to content' | 'Social' | 'Social links' | 'Sorry, something went wrong. Please try again.' + | 'Summary' | 'Switch to English' | 'Switch to French' + | 'Switch to {opt}' | 'Tags' | 'Theme' | 'Thoughts' | 'Topic' | 'Topics' + | 'Uncategorized' | 'Unknown date' | 'Use dark mode' | 'Use light mode' @@ -82,8 +89,9 @@ export type LocaleKey = | 'errors.unauthorized.message' | 'errors.unauthorized.title' | 'errors.no-content.message' - | 'go back' | 'less' + | 'min left' + | 'mins left' | 'more' | 'now' | 'pages.about.description' @@ -100,8 +108,11 @@ export type LocaleKey = | 'pages.work.title' | 'present' | 'refresh the page' + | 'toru.status.playing' + | 'toru.status.paused' | '{document} details' | '{length} min read' + | '{length} words' | '{months} months' | '{month} month' | '{stars} stars'