Compare commits

...

25 Commits

Author SHA1 Message Date
Julien Calixte
c06253e509 chore: migrate oxlint disable comments 2026-03-28 09:52:25 +01:00
Julien Calixte
767093c008 chore: install migrate oxlint skill 2026-03-28 09:49:50 +01:00
Julien Calixte
3a32cb5948 feat: change place for atproto 2026-03-28 09:45:39 +01:00
Julien Calixte
5f48aa5690 chore: lint and fmt 2026-03-28 09:38:55 +01:00
Julien Calixte
8e8706e258 chore: init oxc 2026-03-28 09:34:04 +01:00
Julien Calixte
d457fd4064 docs: rolldown and oxc 2026-03-22 02:44:11 +01:00
Julien Calixte
d1bb9fa182 chore: minify with esbuild 2026-03-22 00:50:54 +01:00
Julien Calixte
80170b9f62 chore: try with regular minifier 2026-03-22 00:49:05 +01:00
Julien Calixte
ed5da7844a chore: try fixing Safari 2026-03-22 00:32:16 +01:00
Julien Calixte
12676135f6 prune nixpacks 2026-03-21 23:03:36 +01:00
Julien Calixte
32f79785a8 fix: prevent stacking a duplicate of the main note when clicking a self-link 2026-03-21 22:59:10 +01:00
Julien Calixte
c0b1a33c69 deps: upgrade vite 2026-03-21 22:46:46 +01:00
Julien Calixte
1fc66289a4 test: init analytics 2026-03-21 21:15:04 +01:00
Julien Calixte
db27b03f21 fix: set width so it doesn't take to much space with larger images 2026-03-21 12:06:22 +01:00
Julien Calixte
72b704a54d feat: small change for deployment 2026-03-21 11:15:17 +01:00
Julien Calixte
b6d5ad5d4b fix: add workbox-window and workbox-build as direct deps for pnpm v10 strict resolution 2026-03-21 11:11:44 +01:00
Julien Calixte
694c2fcae9 chore: replace nixpacks with Dockerfile for faster cached builds 2026-03-21 11:06:58 +01:00
Julien Calixte
dfdd646eb1 deps: migrate to api.remanso.space for everything now 2026-03-21 10:49:39 +01:00
Julien Calixte
3c736124e8 chore: approve build 2026-03-21 09:04:58 +01:00
Julien Calixte
53c444ed72 fix: use smoother ease-out-expo curve for logo view transition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 21:05:39 +01:00
Julien Calixte
29e56304c4 feat: add View Transitions API hero animation for favicon between pages
The favicon animates from its large position in the WelcomeWorld title
to the smaller header position in PublicNoteListView and PublicNoteListByDidView.
2026-03-19 18:43:26 +01:00
Julien Calixte
ddabe5082d feat: show skeleton loaders while ATProto identity resolves
- Show skeleton in PublicNoteView and StackedPublicNote while note
  content is pending author resolution
- Show skeleton h1 in PublicNoteListByDidView while author loads
- Show skeleton in SignInAtproto until auth state is known
- Load cached session from IndexedDB before OAuth restore so the
  homepage resolves immediately without waiting for network
2026-03-19 18:12:52 +01:00
Julien Calixte
52561496b4 fix: change the edit button 2026-03-19 17:40:50 +01:00
Julien Calixte
0ed2906782 fix: use correct capture group index for tabler icon name 2026-03-17 23:50:21 +01:00
Julien Calixte
944b128894 feat: add icons, better suited than emojis 2026-03-17 23:43:29 +01:00
127 changed files with 2453 additions and 1313 deletions

View File

@@ -0,0 +1,8 @@
{
"name": "remanso-skills",
"version": "1.0.0",
"description": "Local skills for the Remanso project",
"author": {
"name": "julien"
}
}

View File

@@ -0,0 +1,196 @@
---
name: migrate-oxlint
description: Guide for migrating a project from ESLint to Oxlint. Use when asked to migrate, convert, or switch a JavaScript/TypeScript project's linter from ESLint to Oxlint.
---
This skill guides you through migrating a JavaScript/TypeScript project from ESLint to [Oxlint](https://oxc.rs/docs/guide/usage/linter/).
## Overview
Oxlint is a high-performance linter that implements many popular ESLint rules natively in Rust. It can be used alongside ESLint or as a full replacement.
An official migration tool is available, and will be used by this skill: [`@oxlint/migrate`](https://github.com/oxc-project/oxlint-migrate)
## Step 1: Run Automated Migration
Run the migration tool in the project root:
```bash
npx @oxlint/migrate
```
This reads your ESLint flat config (`eslint.config.js` for example) and generates a `.oxlintrc.json` file from it. It will find your ESLint config file automatically in most cases.
See options below for more info.
### Key Options
| Option | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `--type-aware` | Include type-aware rules from `@typescript-eslint` (will require the `oxlint-tsgolint` package to be installed after migrating) |
| `--with-nursery` | Include experimental rules still under development, may not be fully stable or consistent with ESLint equivalents |
| `--js-plugins [bool]` | Enable/disable ESLint plugin migration via `jsPlugins` (default: enabled) |
| `--details` | List rules that could not be migrated |
| `--replace-eslint-comments` | Convert all `// eslint-disable` comments to `// oxlint-disable` |
| `--output-file <file>` | Specify a different output path (default: `.oxlintrc.json`) |
If your ESLint config is not at the default location, pass the path explicitly:
```bash
npx @oxlint/migrate ./path/to/eslint.config.js
```
## Step 2: Review Generated Config
After migration, review the generated `.oxlintrc.json`.
### Plugin Mapping
The migration tool automatically maps ESLint plugins to oxlint's built-in equivalents. The following table is for reference when reviewing the generated config:
| ESLint Plugin | Oxlint Plugin Name |
| --------------------------------------------------- | ------------------ |
| `@typescript-eslint/eslint-plugin` | `typescript` |
| `eslint-plugin-react` / `eslint-plugin-react-hooks` | `react` |
| `eslint-plugin-import` / `eslint-plugin-import-x` | `import` |
| `eslint-plugin-unicorn` | `unicorn` |
| `eslint-plugin-jsx-a11y` | `jsx-a11y` |
| `eslint-plugin-react-perf` | `react-perf` |
| `eslint-plugin-promise` | `promise` |
| `eslint-plugin-jest` | `jest` |
| `@vitest/eslint-plugin` | `vitest` |
| `eslint-plugin-jsdoc` | `jsdoc` |
| `eslint-plugin-next` | `nextjs` |
| `eslint-plugin-node` | `node` |
| `eslint-plugin-vue` | `vue` |
Default plugins (enabled when `plugins` field is omitted): `unicorn`, `typescript`, `oxc`.
Setting the `plugins` array explicitly overrides these defaults.
ESLint core rules are usable in oxlint without needing to configure a plugin in the config file.
### Rule Categories
Oxlint groups rules into categories for bulk configuration, though only `correctness` is enabled by default:
```json
{
"categories": {
"correctness": "error",
"suspicious": "warn"
}
}
```
Available categories: `correctness` (default: enabled), `suspicious`, `pedantic`, `perf`, `style`, `restriction`, `nursery`.
Individual rule settings in `rules` override category settings.
`@oxlint/migrate` will turn `correctness` off to avoid enabling additional rules that weren't enabled by your ESLint config. You can choose to enable additional categories after migration if desired.
### Check Unmigrated Rules
Run with `--details` to see which ESLint rules could not be migrated:
```bash
npx @oxlint/migrate --details
```
Review the output and decide whether to keep ESLint for those rules or not. Some rules may be mentioned in the output from `--details` as having equivalents in oxlint that were not automatically mapped by the migration tool. In those cases, consider enabling the equivalent oxlint rule manually after migration.
## Step 3: Install Oxlint
Install the core oxlint package (use `yarn install`, `pnpm install`, `vp install`, `bun install`, etc. depending on your package manager):
```bash
npm install -D oxlint
```
If you want to add the `oxlint-tsgolint` package, if you intend to use type-aware rules that require TypeScript type information:
```bash
npm install -D oxlint-tsgolint
```
No other packages besides the above are needed by default, though you will need to keep/install any additional ESLint plugins that were migrated into `jsPlugins`. Do not add `@oxlint/migrate` to the package.json, it is meant for one-off usage.
## Step 4: Handle Unsupported Features
Some features require manual attention:
- Local plugins (relative path imports): Must be migrated manually to `jsPlugins`
- `eslint-plugin-prettier`: Supported, but very slow. It is recommended to use [oxfmt](https://oxc.rs/docs/guide/usage/formatter) instead, or switch to `prettier --check` as a separate step alongside oxlint.
- `settings` in override configs: Oxlint does not support `settings` inside `overrides` blocks.
- ESLint v9+ plugins: Not all work with oxlint's JS Plugins API, but the majority will.
### Local Plugins
If you have any custom ESLint rules in the project repo itself, you can migrate them manually after running the migration tool by adding them to the `jsPlugins` field in `.oxlintrc.json`:
```json
{
"jsPlugins": ["./path/to/my-plugin.js"],
"rules": {
"local-plugin/rule-name": "error"
}
}
```
### External ESLint Plugins
For ESLint plugins without a built-in oxlint equivalent, use the `jsPlugins` field to load them:
```json
{
"jsPlugins": ["eslint-plugin-custom"],
"rules": {
"custom/my-rule": "warn"
}
}
```
## Step 5: Update CI and Scripts
Replace ESLint commands with oxlint. Path arguments are optional; oxlint defaults to the current working directory.
```bash
# Before
npx eslint src/
npx eslint --fix src/
# After
npx oxlint src/
npx oxlint --fix src/
```
### Common CLI Options
| ESLint | oxlint equivalent |
| ------------------------- | ---------------------------------------------- |
| `eslint .` | `oxlint` (default: lints the cwd) |
| `eslint src/` | `oxlint src/` |
| `eslint --fix` | `oxlint --fix` |
| `eslint --max-warnings 0` | `oxlint --deny-warnings` or `--max-warnings 0` |
| `eslint --format json` | `oxlint --format json` |
Additional oxlint options:
- `--tsconfig <path>`: Specify tsconfig.json path, likely unnecessary unless you have a non-standard name for `tsconfig.json`.
## Tips
- You can run alongside ESLint if necessary: Oxlint is designed to complement ESLint during migration, but with JS Plugins many projects can switch over fully without losing many rules.
- Disable comments work: `// eslint-disable` and `// eslint-disable-next-line` comments are supported by oxlint. Use `--replace-eslint-comments` when running @oxlint/migrate to convert them to `// oxlint-disable` equivalents if desired.
- List available rules: Run `npx oxlint --rules` to see all supported rules, or refer to the [rule documentation](https://oxc.rs/docs/guide/usage/linter/rules.html).
- Schema support: Add `"$schema": "./node_modules/oxlint/configuration_schema.json"` to `.oxlintrc.json` for editor autocompletion if the migration tool didn't do it automatically.
- Output formats: `default`, `stylish`, `json`, `github`, `gitlab`, `junit`, `checkstyle`, `unix`
- Ignore files: `.eslintignore` is supported by oxlint if you have it, but it's recommended to move any ignore patterns into the `ignorePatterns` field in `.oxlintrc.json` for consistency and simplicity. All files and paths ignored via a `.gitignore` file will be ignored by oxlint by default as well.
- If you ran the migration tool multiple times, remove the `.oxlintrc.json.bak` backup file created by the migration tool once you've finished migrating.
- If you are not using any JS Plugins and have replaced your ESLint configuration, you can remove all ESLint packages from your project dependencies.
- Ensure your editor is configured to use oxlint instead of ESLint for linting and error reporting. You may want to install the Oxc extension for your preferred editor. See https://oxc.rs/docs/guide/usage/linter/editors.html for more details.
## References
- [CLI Reference](https://oxc.rs/docs/guide/usage/linter/cli.html)
- [Config File Reference](https://oxc.rs/docs/guide/usage/linter/config-file-reference.html)
- [Complete Oxlint rule list and docs](https://oxc.rs/docs/guide/usage/linter/rules.html)

View File

@@ -0,0 +1,14 @@
{
"name": "remanso-local",
"description": "Local plugins for the Remanso project",
"owner": {
"name": "julien"
},
"plugins": [
{
"name": "remanso-skills",
"description": "Local skills for the Remanso project (migrate-oxlint, etc.)",
"source": "./.agents"
}
]
}

13
.claude/settings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extraKnownMarketplaces": {
"remanso-local": {
"source": {
"source": "directory",
"path": "."
}
}
},
"enabledPlugins": {
"remanso-skills@remanso-local": true
}
}

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.git
.gitignore
*.md
.env*

View File

@@ -1,53 +0,0 @@
require("@rushstack/eslint-patch/modern-module-resolution")
const DEV_TOOL_ACTIVATED =
process.env.NODE_ENV === "production" ? "warn" : "off"
module.exports = {
root: true,
env: {
node: true,
es2022: true,
},
extends: ["plugin:vue/vue3-essential", "@vue/eslint-config-typescript"],
plugins: ["simple-import-sort", "unused-imports"],
rules: {
"no-console": DEV_TOOL_ACTIVATED,
"no-debugger": DEV_TOOL_ACTIVATED,
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/camelcase": "off",
"prettier-vue/prettier": [
"error",
{
semi: false,
singleQuote: true,
trailingComma: "none",
arrowParens: "always",
},
],
"vue/no-v-html": "off",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "vue-demi",
importNames: ["computed"],
message: "Please use computed from vue instead.",
},
],
},
],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
},
overrides: [
{
files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)",
],
},
],
}

9
.oxfmtrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"semi": false,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": []
}

40
.oxlintrc.json Normal file
View File

@@ -0,0 +1,40 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"jsPlugins": [
"eslint-plugin-prettier-vue",
"eslint-plugin-simple-import-sort",
"eslint-plugin-unused-imports"
],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true
},
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "vue-demi",
"importNames": ["computed"],
"message": "Please use computed from vue instead."
}
]
}
],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error"
},
"overrides": [
{
"files": ["**/*.vue"],
"rules": {
"unused-imports/no-unused-imports": "off"
}
}
]
}

View File

@@ -1,3 +0,0 @@
{
"semi": false
}

View File

@@ -51,7 +51,7 @@ src/
│ ├── card/ # Spaced repetition
│ ├── history/ # Edit history tracking
│ ├── atproto/ # ATProto/Bluesky integration (DID resolution, blob URLs)
│ └── post/ # ts-rest API client for public note publishing (api.litenote.li212.fr)
│ └── post/ # ts-rest API client for public note publishing (api.remanso.space)
├── hooks/ # Composition hooks (useMarkdown, useBacklinks, useGitHubContent, etc.)
├── data/ # PouchDB wrapper and data models
├── utils/ # Utilities including custom markdown-it plugins

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# ---- Stage 1: deps (only invalidated when lockfile changes) ----
FROM node:22-alpine AS deps
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# ---- Stage 2: build (invalidated on any source change) ----
FROM node:22-alpine AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm run build
# ---- Stage 3: serve ----
FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:80/ || exit 1

View File

@@ -5,6 +5,7 @@
import { readFileSync, writeFileSync } from "fs"
import { join } from "path"
import { commitTheme } from "./change-theme"
// Chemins vers les fichiers
@@ -28,7 +29,7 @@ let themeConfigContent = readFileSync(themeConfigPath, "utf8")
// Remplacer la valeur du thème sombre
themeConfigContent = themeConfigContent.replace(
/dark:\s*['"][^'"]*['"],/,
`dark: '${newTheme}',`,
`dark: '${newTheme}',`
)
// Écrire le contenu mis à jour dans le fichier
@@ -38,7 +39,7 @@ writeFileSync(themeConfigPath, themeConfigContent)
let appCssContent = readFileSync(appCssPath, "utf8")
appCssContent = appCssContent.replace(
/(\s+)([a-zA-Z0-9-]+)(\s+--prefersdark;)/,
`$1${newTheme}$3`,
`$1${newTheme}$3`
)
writeFileSync(appCssPath, appCssContent)

View File

@@ -5,6 +5,7 @@
import { readFileSync, writeFileSync } from "fs"
import { join } from "path"
import { commitTheme } from "./change-theme"
// Chemins vers les fichiers
@@ -29,7 +30,7 @@ let themeConfigContent = readFileSync(themeConfigPath, "utf8")
// Remplacer la valeur du thème clair
themeConfigContent = themeConfigContent.replace(
/light:\s*['"][^'"]*['"],/,
`light: '${newTheme}',`,
`light: '${newTheme}',`
)
// Écrire le contenu mis à jour dans le fichier
@@ -39,7 +40,7 @@ writeFileSync(themeConfigPath, themeConfigContent)
let indexContent = readFileSync(indexPath, "utf8")
indexContent = indexContent.replace(
/data-theme="[^"]*"/,
`data-theme="${newTheme}"`,
`data-theme="${newTheme}"`
)
writeFileSync(indexPath, indexContent)
@@ -47,7 +48,7 @@ writeFileSync(indexPath, indexContent)
let appCssContent = readFileSync(appCssPath, "utf8")
appCssContent = appCssContent.replace(
/(\s+)([a-zA-Z0-9-]+)(\s+--default,)/,
`$1${newTheme}$3`,
`$1${newTheme}$3`
)
writeFileSync(appCssPath, appCssContent)

View File

@@ -1,3 +1,3 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
presets: ["@vue/cli-plugin-babel/preset"]
}

View File

@@ -0,0 +1,78 @@
# Rolldown minifier drops `while(` keyword — invalid syntax in Safari
**Status:** To be filed
**Workaround applied:** `build: { minify: "esbuild" }` in `vite.config.mts`
## Summary
Rolldown's minifier drops the `while(`/`for(;` keyword when a `while (x in globalThis)` loop is bundled alongside other module-level variable declarations. The resulting output is a syntax error that crashes Safari.
## Target repositories
- <https://github.com/rolldown/rolldown/issues>
- <https://github.com/oxc-project/oxc/issues> (underlying minifier)
## Environment
| Package | Version |
| -------------------- | --------------------------------------- |
| `vite` | 8.0.1 |
| `rolldown` | 1.0.0-rc.10 |
| `@oxc-project/types` | 0.120.0 |
| Triggered by | `@ark/schema` 0.56.0 / `arktype` 2.1.29 |
## Source
File: `node_modules/@ark/schema/out/shared/registry.js`
```js
let _registryName = "$ark"
let suffix = 2
while (_registryName in globalThis) _registryName = `$ark${suffix++}`
export const registryName = _registryName
```
## Actual minified output
```js
un=Pe(`implementedTraits`),dn=`$ark`,fn=2;dn in globalThis;)dn=`$ark${fn++}`;var pn=dn;
```
The `while(` keyword is missing. The orphaned `)` is a syntax error.
## Expected output
```js
var dn = `$ark`,
fn = 2
for (; dn in globalThis; ) dn = `$ark${fn++}`
var pn = dn
```
## Impact
Safari throws: `SyntaxError: Unexpected keyword 'in'. Expected a ';' following a return statement.`
Chrome and Firefox appear to tolerate the malformed output.
## Reproduction attempts
Direct Rolldown calls (even with the full `arktype` + `@better-fetch/fetch` bundle) do **not** reproduce the bug. It only manifests through Vite's full production build pipeline. This strongly suggests the issue is in the interaction between **Vite's module preprocessing** (dependency pre-bundling, plugin transforms, scope-flattening of ESM modules) and Rolldown's minifier `sequences` optimization pass.
Hypothesis: Vite's pipeline produces a flattened scope where multiple `var` declarations from different original modules are consecutive. When the `sequences` optimizer then tries to fold those `var` declarations into the `for` loop init, it incorrectly drops the `for(var` prefix when the loop condition contains the `in` operator.
## Notes
- Related closed issue: [rolldown/rolldown#8146](https://github.com/rolldown/rolldown/issues/8146) — "Minifier incorrectly merges statements into for-in expression via comma operator"
- To reproduce: run `pnpm build` in this project **without** `build.minify: "esbuild"` — the broken chunk is `getAuthor-*.js`
## Workaround
Add to `vite.config.mts`:
```ts
build: {
minify: "esbuild",
},
```
esbuild correctly outputs `for(;Dt in globalThis;)Dt=\`$ark${za++}\``.

View File

@@ -25,6 +25,11 @@
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/4.0.0/github-markdown.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css"
fetchpriority="low"
/>
</head>
<body>
<noscript>

View File

@@ -9,9 +9,9 @@ status = 200
[[headers]]
for = "/client-metadata.json"
[headers.values]
Access-Control-Allow-Origin = "*"
Content-Type = "application/json"
[headers.values]
Access-Control-Allow-Origin = "*"
Content-Type = "application/json"
[[redirects]]
from = "/client-metadata.json"

View File

@@ -1,2 +0,0 @@
[phases.setup]
nixPkgs = ["nodejs_24", "pnpm"]

View File

@@ -8,7 +8,10 @@
"serve": "vite preview",
"test": "vitest",
"types": "tsc --noEmit",
"lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore --fix src",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"fmt": "oxfmt",
"fmt:check": "oxfmt --check",
"prepare": "husky",
"theme:light": "esno _scripts/change-theme-light.ts",
"theme:dark": "esno _scripts/change-theme-dark.ts",
@@ -21,6 +24,7 @@
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@octokit/core": "^7.0.6",
"@octokit/rest": "^22.0.1",
"@openpanel/web": "^1.3.0",
"@tailwindcss/postcss": "^4.1.16",
"@tanstack/vue-query": "^5.92.9",
"@toycode/markdown-it-class": "^1.2.4",
@@ -53,42 +57,38 @@
"sanitize-html": "^2.17.0",
"vue": "^3.5.18",
"vue-i18n": "^11.1.11",
"vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"workbox-build": "^7.4.0",
"workbox-window": "^7.4.0"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@rushstack/eslint-patch": "^1.14.1",
"@tailwindcss/typography": "^0.5.19",
"@types/fontfaceobserver": "^2.1.3",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.15.24",
"@types/pouchdb-browser": "^6.1.5",
"@types/sanitize-html": "^2.16.0",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"@vite-pwa/assets-generator": "^1.0.2",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.28",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"autoprefixer": "^10.4.24",
"daisyui": "^5.5.18",
"dotenv": "^17.2.3",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier-vue": "^5.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.4.1",
"eslint-plugin-vue": "^10.8.0",
"esno": "^4.8.0",
"husky": "^9.1.7",
"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
"prettier": "^3.8.1",
"prettier-vue": "^1.1.2",
"sass": "^1.93.3",
"tailwindcss": "^4.1.16",
"sass": "^1.98.0",
"tailwindcss": "^4.2.2",
"typescript": "~5.9.3",
"vite": "^7.1.12",
"vite-plugin-pwa": "^1.1.0",
"vite": "^8.0.1",
"vite-plugin-pwa": "^1.2.0",
"vitest": "^3.2.4"
}
}

1770
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
allowBuilds:
core-js: true

View File

@@ -1,3 +1,3 @@
module.exports = {
plugins: { "@tailwindcss/postcss": {}, autoprefixer: {} },
plugins: { "@tailwindcss/postcss": {}, autoprefixer: {} }
}

View File

@@ -2,9 +2,7 @@
"client_id": "https://remanso.space/client-metadata.json",
"client_name": "Remanso",
"client_uri": "https://remanso.space",
"redirect_uris": [
"https://remanso.space/"
],
"redirect_uris": ["https://remanso.space/"],
"scope": "atproto transition:generic",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],

View File

@@ -1,9 +1,9 @@
import {
defineConfig,
minimal2023Preset as preset,
minimal2023Preset as preset
} from "@vite-pwa/assets-generator/config"
export default defineConfig({
preset,
images: ["public/favicon.png"],
images: ["public/favicon.png"]
})

10
skills-lock.json Normal file
View File

@@ -0,0 +1,10 @@
{
"version": 1,
"skills": {
"migrate-oxlint": {
"source": "oxc-project/oxc",
"sourceType": "github",
"computedHash": "80ce5201b1ef52d6cabe553a4cacfd6e1db97bad99618216b9cf9318d11d7e64"
}
}
}

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import NewVersion from '@/components/NewVersion.vue'
import { useATProtoLogin } from '@/hooks/useATProtoLogin.hook'
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
import NewVersion from "@/components/NewVersion.vue"
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
const { isReady } = useGitHubLogin()
const { isATProtoReady } = useATProtoLogin()
@@ -18,7 +18,23 @@ const { isATProtoReady } = useATProtoLogin()
<style lang="scss">
#main-app {
height: 100vh;
width: 100%;
display: flex;
flex: 1;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.25s;
}
::view-transition-group(remanso-logo) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
::view-transition-old(remanso-logo),
::view-transition-new(remanso-logo) {
object-fit: contain;
}
</style>

View File

@@ -0,0 +1,9 @@
import { OpenPanel } from "@openpanel/web"
export const op = new OpenPanel({
apiUrl: "https://api.panel.apoena.dev",
clientId: "038a6aac-19bb-4a7f-9aae-2d0201fead5b",
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true
})

View File

@@ -1,4 +1,4 @@
import { createEventBus } from 'retrobus'
import { createEventBus } from "retrobus"
interface EventBusParams {
fileSha: string

View File

@@ -1,4 +1,4 @@
import { createEventBus } from 'retrobus'
import { createEventBus } from "retrobus"
interface EventBusParams {
user: string

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { onBeforeMount, ref } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
import { signIn } from '@/modules/user/service/signIn'
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { signIn } from "@/modules/user/service/signIn"
const route = useRoute()
const router = useRouter()
@@ -16,14 +16,14 @@ onBeforeMount(async () => {
if (code) {
const token = await signIn(code.toString())
if ('error' in token) {
if ("error" in token) {
hasError.value = true
} else {
token.access_token
saveCredentials(token)
}
router.replace({ name: 'Home' })
router.replace({ name: "Home" })
}
})
</script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { useRouter, type RouteLocationRaw } from "vue-router"
import { type RouteLocationRaw, useRouter } from "vue-router"
const props = withDefaults(
defineProps<{ fallback?: RouteLocationRaw; preferFallback?: boolean }>(),
{ preferFallback: true },
{ preferFallback: true }
)
const router = useRouter()

View File

@@ -6,9 +6,10 @@ import {
onMounted,
onUnmounted,
toRefs,
watch,
watch
} from "vue"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
import StackedNote from "@/components/StackedNote.vue"
import { useLinks } from "@/hooks/useLinks.hook"
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
@@ -19,10 +20,9 @@ import { useVisitRepo } from "@/modules/history/hooks/useVisitRepo.hook"
import CacheAllNotes from "@/modules/note/components/CacheAllNote.vue"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { useUserSettings } from "@/modules/user/hooks/useUserSettings.hook"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
const HeaderNote = defineAsyncComponent(
() => import("@/components/HeaderNote.vue"),
() => import("@/components/HeaderNote.vue")
)
const props = withDefaults(
@@ -38,8 +38,8 @@ const props = withDefaults(
content: null,
parseContent: true,
withContent: true,
withHeader: true,
},
withHeader: true
}
)
const user = computed(() => props.user)
@@ -61,7 +61,7 @@ const renderedContent = computed(() =>
? props.parseContent
? toHTML(props.content)
: props.content
: store.readme,
: store.readme
)
const isLoading = computed(() => renderedContent.value === undefined)
@@ -73,7 +73,7 @@ watch(
await nextTick()
listenToClick()
},
{ immediate: true },
{ immediate: true }
)
watch(
@@ -81,7 +81,7 @@ watch(
() => {
store.setUserRepo(props.user, props.repo)
},
{ immediate: true },
{ immediate: true }
)
onMounted(() => visitRepo())

View File

@@ -9,7 +9,7 @@ const store = useUserRepoStore()
const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? [])
const sortedFontFamilies = computed(() =>
[...fontFamilies.value].sort((a, b) => a.localeCompare(b)),
[...fontFamilies.value].sort((a, b) => a.localeCompare(b))
)
const fontSizes = Array.from({ length: 7 }, (_, i) => `${9 + i * 2}pt`)
</script>

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { useRouter } from "vue-router"
const { push } = useRouter()
const back = () =>
push({
name: 'Home'
name: "Home"
})
</script>

View File

@@ -7,7 +7,7 @@ const goHome = () => router.push({ name: "Home" })
<template>
<a class="btn btn-ghost btn-circle btn-lg" @click="goHome">
<img src="/favicon.png" alt="Remanso icon" />
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
</a>
</template>
@@ -15,4 +15,10 @@ const goHome = () => router.push({ name: "Home" })
img {
box-shadow: none;
}
.remanso-logo {
width: 32px;
height: 32px;
view-transition-name: remanso-logo;
}
</style>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup>
const url = new URL('https://github.com/login/oauth/authorize')
url.searchParams.append('client_id', 'Iv1.87be14adcc912fa0')
url.searchParams.append('redirect_uri', location.href)
url.searchParams.append('scope', 'repo')
const url = new URL("https://github.com/login/oauth/authorize")
url.searchParams.append("client_id", "Iv1.87be14adcc912fa0")
url.searchParams.append("redirect_uri", location.href)
url.searchParams.append("scope", "repo")
</script>
<template>

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { toShortDid } from "@/modules/atproto/shortDid"
import { slugify } from "@/utils/slugify"
import { vInfiniteScroll } from "@vueuse/components"
import { toShortDid } from "@/modules/atproto/shortDid"
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { slugify } from "@/utils/slugify"
defineProps<{
notes: PublicNoteListItem[]
canLoadMore: boolean
@@ -28,8 +29,8 @@ defineSlots<{
params: {
shortDid: toShortDid(note.did),
rkey: note.rkey,
slug: slugify(note.title),
},
slug: slugify(note.title)
}
}"
class="btn btn-link"
>{{ note.title }}</router-link

View File

@@ -26,8 +26,8 @@ const getStyle = (seed: string) => {
name: 'FluxNoteView',
params: {
user: username,
repo: favoriteRepo.name,
},
repo: favoriteRepo.name
}
}"
class="btn"
:style="getStyle(`${favoriteRepo.name}-${username}`)"

View File

@@ -3,15 +3,16 @@ import { ref } from "vue"
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
const { handle, isLoggedIn, signIn, signOut } = useATProtoLogin()
const { handle, isLoggedIn, isATProtoReady, signIn, signOut } =
useATProtoLogin()
withDefaults(
defineProps<{
withSignOut?: boolean
}>(),
{
withSignOut: true,
},
withSignOut: true
}
)
const inputHandle = ref("")
@@ -24,13 +25,14 @@ const onSignIn = () => {
</script>
<template>
<div v-if="isLoggedIn" class="sign-in-atproto is-signed-in">
<div v-if="!isATProtoReady" class="skeleton h-8 w-40"></div>
<div v-else-if="isLoggedIn" class="sign-in-atproto is-signed-in">
<span>{{ handle }}</span>
<button class="btn btn-sm" @click="signOut" v-if="withSignOut">
Sign out
</button>
</div>
<div v-else class="sign-in-atproto join">
<div v-else-if="!isLoggedIn" class="sign-in-atproto join">
<input
v-model="inputHandle"
class="input input-sm join-item"

View File

@@ -5,7 +5,7 @@ import {
nextTick,
onMounted,
ref,
watch,
watch
} from "vue"
import { useEditionMode } from "@/hooks/useEditionMode"
@@ -13,20 +13,20 @@ import { useFile } from "@/hooks/useFile.hook"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
import { useImages } from "@/hooks/useImages.hook"
import { useLinks } from "@/hooks/useLinks.hook"
import { runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { useTitleNotes } from "@/hooks/useTitleNotes.hook"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
import { filenameToNoteTitle } from "@/utils/noteTitle"
import { runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
const LinkedNotes = defineAsyncComponent(
() => import("@/components/LinkedNotes.vue"),
() => import("@/components/LinkedNotes.vue")
)
const EditNote = defineAsyncComponent(
() => import("@/modules/note/components/EditNote.vue"),
() => import("@/modules/note/components/EditNote.vue")
)
const props = defineProps<{
@@ -50,7 +50,7 @@ const {
rawContent,
getRawContent,
saveCacheNote,
getEditedSha,
getEditedSha
} = useFile(sha)
const initialRawContent = ref<string | null>(null)
const className = computed(() => `stacked-note-${props.index}`)
@@ -67,7 +67,7 @@ const breadcrumbs = computed(() => displayedTitle.value.split(" / "))
const { updateFile } = useGitHubContent({
user: user.value,
repo: repo.value,
repo: repo.value
})
onMounted(async () => {
@@ -115,7 +115,7 @@ watch(mode, async (newMode) => {
const newSha = await updateFile({
content: rawContent.value,
path: path.value,
sha: editedSha,
sha: editedSha
})
if (!newSha) {
@@ -125,7 +125,7 @@ watch(mode, async (newMode) => {
}
await saveCacheNote(encodeUTF8ToBase64(rawContent.value), {
editedSha: newSha,
editedSha: newSha
})
initialRawContent.value = rawContent.value
})
@@ -137,7 +137,7 @@ watch(mode, async (newMode) => {
:class="{
[className]: true,
overlay: displayNoteOverlay,
[`note-${sha}`]: true,
[`note-${sha}`]: true
}"
>
<a
@@ -159,9 +159,11 @@ watch(mode, async (newMode) => {
<button
class="action button is-text is-light"
:class="{ 'is-link': mode === 'edit' }"
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
@click="toggleMode"
>
<svg
v-if="mode === 'read'"
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-edit"
width="24"
@@ -182,37 +184,29 @@ watch(mode, async (newMode) => {
/>
<path d="M16 5l3 3" />
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-device-floppy"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"
/>
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M14 4l0 4l-6 0l0 -4" />
</svg>
</button>
<div v-if="mode === 'edit'" class="edit">
<edit-note v-model="rawContent" />
<button
class="action button is-text is-light"
:class="{ 'is-link': mode === 'edit' }"
@click="toggleMode"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-edit"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"
/>
<path
d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"
/>
<path d="M16 5l3 3" />
</svg>
</button>
</div>
<div v-if="mode === 'read'" class="note-content" v-html="content"></div>
</section>

View File

@@ -1,16 +1,19 @@
<script lang="ts" setup>
import { computedAsync } from "@vueuse/core"
import { computed, nextTick, ref, watch } from "vue"
import { errorMessage } from "@/utils/notif"
import { useRoute } from "vue-router"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { computedAsync } from "@vueuse/core"
import { getUrl } from "@/modules/atproto/getUrl"
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
import { getAuthor } from "@/modules/atproto/getAuthor"
import { fromShortDid } from "@/modules/atproto/shortDid"
import { getUrl } from "@/modules/atproto/getUrl"
import { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
import { fromShortDid } from "@/modules/atproto/shortDid"
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
import { errorMessage } from "@/utils/notif"
const props = defineProps<{
didrkey: string
@@ -26,14 +29,22 @@ const index = computed(() => props.index)
const author = computedAsync(async () => getAuthor(did.value))
const url = computedAsync(async () =>
getUrl({ did: did.value, rkey: rkey.value }),
getUrl({ did: did.value, rkey: rkey.value })
)
const className = computed(() => `stacked-note-${props.index}`)
const titleClassName = computed(() => `title-${className.value}`)
const route = useRoute()
const mainNoteId = computed(
() => `${route.params.shortDid}-${route.params.rkey}`
)
const { scrollToFocusedNote } = useRouteQueryStackedNotes()
const { listenToClick } = useATProtoLinks(className.value, didrkey)
const { listenToClick } = useATProtoLinks(className.value, {
currentAtUri: didrkey,
mainNoteId
})
const { displayNoteOverlay } = useNoteOverlay(className.value, index)
const noteNotFound = ref(false)
@@ -59,10 +70,10 @@ const content = computed(() =>
? toHTML(
withATProtoImages(noteRecord.value.value.content, {
pds: author.value.pds,
did: did.value,
}),
did: did.value
})
)
: "",
: ""
)
watch(
@@ -71,7 +82,7 @@ watch(
await nextTick()
listenToClick()
},
{ immediate: true },
{ immediate: true }
)
</script>
@@ -81,7 +92,7 @@ watch(
:class="{
[className]: true,
overlay: displayNoteOverlay,
[`note-${classNameId}`]: true,
[`note-${classNameId}`]: true
}"
>
<a
@@ -99,7 +110,8 @@ watch(
<div v-if="noteNotFound" class="alert alert-error">
This note no longer exists.
</div>
<div class="note-content" v-else v-html="content"></div>
<div class="note-content" v-else-if="content" v-html="content"></div>
<skeleton-loader v-else-if="!noteNotFound" />
</section>
</div>
</template>

View File

@@ -14,7 +14,7 @@ const { userInput, repoInput, submit } = useForm()
<template>
<div class="welcome-world">
<h1 class="title is-1">
<img src="/favicon.png" alt="Remanso icon" />
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
Remanso
</h1>
@@ -23,7 +23,6 @@ const { userInput, repoInput, submit } = useForm()
<last-visited />
<div class="get-started">
<sign-in-atproto :with-sign-out="false" />
<sign-in-github />
<router-link v-if="isLogged" :to="{ name: 'RepoList' }" class="btn btn-sm"
>Manage your repos</router-link
@@ -72,10 +71,11 @@ const { userInput, repoInput, submit } = useForm()
<a href="https://apoena.dev" target="_blank" rel="noopener noreferrer"
>apoena</a
>
<sign-in-atproto :with-sign-out="false" />
<router-link
:to="{
name: 'FluxNoteView',
params: { user: 'remanso-space', repo: 'getting-started' },
params: { user: 'remanso-space', repo: 'getting-started' }
}"
class="btn btn-sm"
>Get started</router-link
@@ -93,6 +93,10 @@ h1 {
}
}
.remanso-logo {
view-transition-name: remanso-logo;
}
.welcome-world {
padding: 1rem;
margin: auto;

View File

@@ -4,8 +4,8 @@ export const getNoteWidth = () => {
if (cached === undefined) {
cached = parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
"--note-width",
),
"--note-width"
)
)
}
return cached

View File

@@ -1,11 +1,11 @@
export enum DataType {
GithubAccessToken = 'GithubAccessToken',
FavoriteRepo = 'FavoriteRepo',
SavedRepo = 'SavedRepo',
Note = 'Note',
BacklinkNote = 'BacklinkNote',
RepetitionCard = 'RepetitionCard',
History = 'History',
UserSettings = 'UserSettings',
AtprotoSession = 'AtprotoSession'
GithubAccessToken = "GithubAccessToken",
FavoriteRepo = "FavoriteRepo",
SavedRepo = "SavedRepo",
Note = "Note",
BacklinkNote = "BacklinkNote",
RepetitionCard = "RepetitionCard",
History = "History",
UserSettings = "UserSettings",
AtprotoSession = "AtprotoSession"
}

View File

@@ -15,13 +15,13 @@ interface GetAllParams {
}
class Data {
// eslint-disable-next-line @typescript-eslint/ban-types
// oxlint-disable-next-line typescript/ban-types
private readonly locale: PouchDB.Database<{}> | null = null
constructor() {
try {
this.locale = new PouchDb("remanso", {
adapter: "indexeddb",
adapter: "indexeddb"
})
} catch (error) {
console.warn("data error", error)
@@ -40,7 +40,7 @@ class Data {
}
public async update<DT extends DataType, T extends Model<DT>>(
model: T,
model: T
): Promise<boolean> {
try {
if (!model._id) {
@@ -71,7 +71,7 @@ class Data {
}
const result = await this.locale?.put({
...doc,
_deleted: true,
_deleted: true
})
return result?.ok ?? false
} catch {
@@ -80,7 +80,7 @@ class Data {
}
public async get<DT extends DataType, T extends Model<DT>>(
id: string,
id: string
): Promise<T | null> {
try {
return ((await this.locale?.get(id)) as T) || null
@@ -91,7 +91,7 @@ class Data {
public async getOrCreate<DT extends DataType, T extends Model<DT>>(
id: string,
initialValue: T,
initialValue: T
): Promise<T> {
const element = await this.get<DT, T>(id)
@@ -108,7 +108,7 @@ class Data {
prefix,
includeDocs = true,
includeAttachments = false,
keys = [],
keys = []
}: GetAllParams): Promise<T[]> {
if (!this.locale) {
return []
@@ -118,7 +118,7 @@ class Data {
const response = await this.locale.allDocs({
include_docs: includeDocs,
attachments: includeAttachments,
keys: keys.map((key) => this.generateId(prefix, key)),
keys: keys.map((key) => this.generateId(prefix, key))
})
if (includeDocs) {
@@ -148,7 +148,7 @@ class Data {
include_docs: includeDocs,
attachments: includeAttachments,
startkey: prefix ? prefix : undefined,
endkey: prefix ? `${prefix}\ufff0` : undefined,
endkey: prefix ? `${prefix}\ufff0` : undefined
})
return response.rows.map((row) => row.doc) as T[]

View File

@@ -1,5 +1,5 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
import { DataType } from "@/data/DataType.enum"
import { Model } from "@/data/models/Model"
export interface AtprotoSession extends Model<DataType.AtprotoSession> {
did: string

View File

@@ -1,5 +1,5 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
import { DataType } from "@/data/DataType.enum"
import { Model } from "@/data/models/Model"
export interface GithubAccessToken extends Model<DataType.GithubAccessToken> {
username: string

View File

@@ -1,5 +1,5 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
import { DataType } from "@/data/DataType.enum"
import { Model } from "@/data/models/Model"
export interface History extends Model<DataType.History> {
repos: ReadonlyArray<{

View File

@@ -1,4 +1,4 @@
import { DataType } from '../DataType.enum'
import { DataType } from "../DataType.enum"
export interface Model<DT extends DataType> {
_id?: string

View File

@@ -1,16 +1,21 @@
import { ComputedRef, onUnmounted, Ref, toValue } from "vue"
import { isExternalLink } from "@/utils/link"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { parseAtUri } from "@/modules/atproto/parseAtUri"
import { toShortDid } from "@/modules/atproto/shortDid"
import { router } from "@/router/router"
import { isExternalLink } from "@/utils/link"
export const useATProtoLinks = (
className: ComputedRef<string> | string,
currentAtUri?: Ref<string> | string,
options: {
currentAtUri?: Ref<string> | string | ComputedRef<string>
mainNoteId: Ref<string> | string | ComputedRef<string>
}
) => {
const { addStackedNote } = useRouteQueryStackedNotes()
const { addStackedNote, scrollToFocusedNote } = useRouteQueryStackedNotes()
const { currentAtUri, mainNoteId } = options
const linkNote = (event: Event) => {
const target = event.target as HTMLElement
const href = target.getAttribute("href")
@@ -33,7 +38,7 @@ export const useATProtoLinks = (
if (href.startsWith(window.location.origin)) {
const { params } = router.resolve(
href.replace(window.location.origin, ""),
href.replace(window.location.origin, "")
)
if (!params.shortDid || !params.rkey) {
@@ -44,10 +49,15 @@ export const useATProtoLinks = (
? `${params.shortDid}-${params.rkey}-${params.slug}`
: `${params.shortDid}-${params.rkey}`
if (noteId === toValue(mainNoteId)) {
scrollToFocusedNote(null)
return
}
addStackedNote(
toValue(currentAtUri) ?? "",
noteId,
`${params.shortDid}-${params.rkey}`,
`${params.shortDid}-${params.rkey}`
)
return
}
@@ -56,6 +66,11 @@ export const useATProtoLinks = (
const { did, rkey } = parseAtUri(href)
const noteId = `${toShortDid(did)}-${rkey}`
if (noteId === toValue(mainNoteId)) {
scrollToFocusedNote(null)
return
}
addStackedNote(toValue(currentAtUri) ?? "", noteId)
}
}
@@ -96,6 +111,6 @@ export const useATProtoLinks = (
})
return {
listenToClick,
listenToClick
}
}

View File

@@ -1,8 +1,16 @@
import { computed, ref } from 'vue'
import { computed, ref } from "vue"
import { getAuthor } from '@/modules/atproto/getAuthor'
import { restoreSession, sdkSignOut, signInWithHandle } from '@/modules/atproto/service/atprotoOAuth'
import { clearSession, loadSession, saveSession } from '@/modules/atproto/service/atprotoSession'
import { getAuthor } from "@/modules/atproto/getAuthor"
import {
restoreSession,
sdkSignOut,
signInWithHandle
} from "@/modules/atproto/service/atprotoOAuth"
import {
clearSession,
loadSession,
saveSession
} from "@/modules/atproto/service/atprotoSession"
const did = ref<string | null>(null)
const handle = ref<string | null>(null)
@@ -10,21 +18,26 @@ const handle = ref<string | null>(null)
let init = true
const initializeAuth = async () => {
const session = await restoreSession()
// Load cached session from IndexedDB first (fast, local) so the UI can render immediately
const stored = await loadSession()
did.value = stored?.did ?? ""
handle.value = stored?.handle ?? ""
// Then restore OAuth session in the background (may involve network)
const session = await restoreSession()
if (session) {
const author = await getAuthor(session.did)
const resolvedHandle = author?.handle ?? ''
const resolvedHandle = author?.handle ?? ""
did.value = session.did
handle.value = resolvedHandle
await saveSession(session.did, resolvedHandle)
window.history.replaceState(null, '', window.location.pathname + window.location.search)
} else {
const stored = await loadSession()
did.value = stored?.did ?? ''
handle.value = stored?.handle ?? ''
window.history.replaceState(
null,
"",
window.location.pathname + window.location.search
)
}
}
@@ -46,8 +59,8 @@ export const useATProtoLogin = () => {
await sdkSignOut(did.value)
}
await clearSession()
did.value = ''
handle.value = ''
did.value = ""
handle.value = ""
}
return {
@@ -56,6 +69,6 @@ export const useATProtoLogin = () => {
isLoggedIn,
isATProtoReady,
signIn,
signOut,
signOut
}
}

View File

@@ -1,10 +1,10 @@
import { useAsyncState } from '@vueuse/core'
import { ComputedRef, onUnmounted, toValue } from 'vue'
import { useAsyncState } from "@vueuse/core"
import { ComputedRef, onUnmounted, toValue } from "vue"
import { backlinkEventBus } from '@/bus/backlinkEventBus'
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { BacklinkNote } from '@/modules/note/models/BacklinkNote'
import { backlinkEventBus } from "@/bus/backlinkEventBus"
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { BacklinkNote } from "@/modules/note/models/BacklinkNote"
export const useBacklinks = (sha: string | ComputedRef<string>) => {
sha = toValue(sha)

View File

@@ -1,5 +1,6 @@
import { ref, Ref, toValue, onUnmounted } from "vue"
import { useDebounceFn } from "@vueuse/core"
import { onUnmounted, Ref, ref, toValue } from "vue"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
const CHECKBOX_PATTERN = /\[([ xX])\]/g
@@ -7,7 +8,7 @@ const CHECKBOX_PATTERN = /\[([ xX])\]/g
const setCheckboxInMarkdown = (
markdown: string,
index: number,
checked: boolean,
checked: boolean
): string => {
let currentIndex = 0
@@ -21,7 +22,7 @@ const setCheckboxInMarkdown = (
const findCheckboxIndex = (
container: Element,
checkbox: HTMLInputElement,
checkbox: HTMLInputElement
): number => {
const allCheckboxes = container.querySelectorAll('input[type="checkbox"]')
return Array.from(allCheckboxes).indexOf(checkbox)
@@ -34,7 +35,7 @@ export const useCheckboxCommit = ({
initialContent,
initialSha,
containerSelector,
debounceMs = 1000,
debounceMs = 1000
}: {
user: string
repo: string
@@ -76,7 +77,7 @@ export const useCheckboxCommit = ({
const newSha = await updateFile({
content: pendingContent.value,
path: pathValue,
sha: currentSha.value,
sha: currentSha.value
})
if (newSha) {
@@ -109,7 +110,7 @@ export const useCheckboxCommit = ({
pendingContent.value = setCheckboxInMarkdown(
pendingContent.value,
index,
target.checked,
target.checked
)
hasPendingChanges.value = true
@@ -142,6 +143,6 @@ export const useCheckboxCommit = ({
isCommitting,
hasPendingChanges,
syncContent,
listenToCheckboxes,
listenToCheckboxes
}
}

View File

@@ -1,18 +1,18 @@
import { watch } from 'vue'
import { watch } from "vue"
import { backlinkEventBus } from '@/bus/backlinkEventBus'
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { useFile } from '@/hooks/useFile.hook'
import { Backlink } from '@/modules/note/models/Backlink'
import { BacklinkNote } from '@/modules/note/models/BacklinkNote'
import { resolvePath } from '@/modules/repo/services/resolvePath'
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store'
import { isExternalLink } from '@/utils/link'
import { filenameToNoteTitle } from '@/utils/noteTitle'
import { confirmMessage } from '@/utils/notif'
import { backlinkEventBus } from "@/bus/backlinkEventBus"
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { useFile } from "@/hooks/useFile.hook"
import { Backlink } from "@/modules/note/models/Backlink"
import { BacklinkNote } from "@/modules/note/models/BacklinkNote"
import { resolvePath } from "@/modules/repo/services/resolvePath"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { isExternalLink } from "@/utils/link"
import { filenameToNoteTitle } from "@/utils/noteTitle"
import { confirmMessage } from "@/utils/notif"
const isMarkdown = (filename?: string) => filename?.endsWith('.md') ?? false
const isMarkdown = (filename?: string) => filename?.endsWith(".md") ?? false
export const useComputeBacklinks = () => {
const store = useUserRepoStore()
@@ -51,18 +51,18 @@ export const useComputeBacklinks = () => {
}
const parser = new DOMParser()
const htmlDoc = parser.parseFromString(note, 'text/html')
const htmlDoc = parser.parseFromString(note, "text/html")
const links = htmlDoc.querySelectorAll('a')
const links = htmlDoc.querySelectorAll("a")
for (const link of links) {
const href = link.getAttribute('href') ?? ''
const href = link.getAttribute("href") ?? ""
if (isExternalLink(href) || !isMarkdown(href)) {
continue
}
const path = resolvePath(file.path ?? '', href)
const path = resolvePath(file.path ?? "", href)
const backlinkFile = store.files.find((file) => file.path === path)
if (!backlinkFile?.sha || !backlinkFile?.path) {
@@ -77,14 +77,14 @@ export const useComputeBacklinks = () => {
if (!notifiedForComputation) {
notifiedForComputation = true
confirmMessage('Updating backlinks...')
confirmMessage("Updating backlinks...")
}
backlinks.set(backlinkFile.sha, [
...previousBacklinks,
{
sha: file.sha,
title: filenameToNoteTitle(file.path ?? '')
title: filenameToNoteTitle(file.path ?? "")
}
])
}

View File

@@ -1,16 +1,16 @@
import { useMagicKeys } from '@vueuse/core'
import { ref, watch } from 'vue'
import { useMagicKeys } from "@vueuse/core"
import { ref, watch } from "vue"
export const useEditionMode = () => {
const mode = ref<'read' | 'edit'>('read')
const mode = ref<"read" | "edit">("read")
const toggleMode = () => {
mode.value = mode.value === 'read' ? 'edit' : 'read'
mode.value = mode.value === "read" ? "edit" : "read"
}
const { escape } = useMagicKeys()
watch(escape, () => {
if (mode.value === 'edit') {
if (mode.value === "edit") {
toggleMode()
}
})

View File

@@ -17,18 +17,18 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
const {
render,
renderFromUTF8,
getRawContent: getRawContentFromFile,
getRawContent: getRawContentFromFile
} = markdownBuilder(shaValue)
const { getCachedNote, saveCacheNote } = prepareNoteCache(
shaValue,
toValue(path),
toValue(path)
)
const fromCache = ref(false)
const rawContent = ref("")
const content = computed(() =>
rawContent.value ? renderFromUTF8(rawContent.value) : "",
rawContent.value ? renderFromUTF8(rawContent.value) : ""
)
const getEditedSha = async () => {
@@ -55,7 +55,7 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
}
saveCacheNote(fileContent)
rawContent.value = getRawContentFromFile(fileContent)
},
}
)
}
@@ -111,6 +111,6 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
getCachedFileContent,
getEditedSha,
fromCache,
saveCacheNote,
saveCacheNote
}
}

View File

@@ -1,17 +1,18 @@
import { computedAsync } from "@vueuse/core"
import { computed, Ref, ref, watch } from "vue"
import { Author, getAuthors } from "@/modules/atproto/getAuthor"
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref, watch } from "vue"
export function useFollowingNoteList(
dids: Ref<Set<string>>,
enabled: Ref<boolean>,
enabled: Ref<boolean>
) {
const isLoading = ref(false)
const notes = ref<PublicNoteListItem[]>([])
const cursor = ref<string | null | undefined>(null)
const canLoadMore = computed(
() => dids.value.size > 0 && cursor.value !== undefined,
() => dids.value.size > 0 && cursor.value !== undefined
)
const onLoadMore = async () => {
@@ -22,7 +23,7 @@ export function useFollowingNoteList(
const body: { dids: string[]; limit: number; cursor?: string } = {
dids: [...dids.value],
limit: 20,
limit: 20
}
if (cursor.value) {
@@ -32,7 +33,7 @@ export function useFollowingNoteList(
const response = await fetch("https://api.remanso.space/notes/feed", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
body: JSON.stringify(body)
})
const data: { notes: PublicNoteListItem[]; cursor?: string } =

View File

@@ -1,6 +1,6 @@
import { Ref, ref, watch } from 'vue'
import { Ref, ref, watch } from "vue"
import { getFollows } from '@/modules/atproto/service/getFollows'
import { getFollows } from "@/modules/atproto/service/getFollows"
export const useFollows = (did: Ref<string | null>) => {
const follows = ref<Set<string>>(new Set())
@@ -14,7 +14,7 @@ export const useFollows = (did: Ref<string | null>) => {
follows.value = new Set()
}
},
{ immediate: true },
{ immediate: true }
)
return { follows }

View File

@@ -1,9 +1,9 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ref } from "vue"
import { useRouter } from "vue-router"
export const useForm = () => {
const userInput = ref('')
const repoInput = ref('')
const userInput = ref("")
const repoInput = ref("")
const { push } = useRouter()
const submit = () => {
@@ -12,7 +12,7 @@ export const useForm = () => {
}
push({
name: 'FluxNoteView',
name: "FluxNoteView",
params: {
user: userInput.value,
repo: repoInput.value

View File

@@ -4,7 +4,7 @@ import { confirmMessage, errorMessage } from "@/utils/notif"
export const useGitHubContent = ({
user,
repo,
repo
}: {
user: string
repo: string
@@ -12,7 +12,7 @@ export const useGitHubContent = ({
const putFile = async ({
content,
path,
sha,
sha
}: {
content: string
path: string
@@ -29,8 +29,8 @@ export const useGitHubContent = ({
path,
message: `Updating ${path} from Remanso`,
content: encodeUTF8ToBase64(content),
sha,
},
sha
}
)
confirmMessage("✅ Note saved")
@@ -48,6 +48,6 @@ export const useGitHubContent = ({
updateFile: async (props: { content: string; path: string; sha: string }) =>
putFile(props),
createFile: async (props: { content: string; path: string }) =>
putFile(props),
putFile(props)
}
}

View File

@@ -1,8 +1,8 @@
import { computed, ref } from 'vue'
import { computed, ref } from "vue"
import { GithubToken } from '@/modules/user/interfaces/GithubToken'
import { getAccessToken, saveAccessToken } from '@/modules/user/service/signIn'
import { confirmMessage } from '@/utils/notif'
import { GithubToken } from "@/modules/user/interfaces/GithubToken"
import { getAccessToken, saveAccessToken } from "@/modules/user/service/signIn"
import { confirmMessage } from "@/utils/notif"
const username = ref<string | null>(null)
const accessToken = ref<string | null>(null)
@@ -11,8 +11,8 @@ let init = true
const saveAccessTokenToLocal = async () => {
const response = await getAccessToken()
username.value = response?.username || ''
accessToken.value = response?.token || ''
username.value = response?.username || ""
accessToken.value = response?.token || ""
}
const saveCredentials = async (token: GithubToken): Promise<void> => {

View File

@@ -1,10 +1,10 @@
import { computed, watch } from 'vue'
import { computed, watch } from "vue"
import { useFile } from '@/hooks/useFile.hook'
import { resolvePath } from '@/modules/repo/services/resolvePath'
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store'
import { useFile } from "@/hooks/useFile.hook"
import { resolvePath } from "@/modules/repo/services/resolvePath"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
const SRC_PREFIX = 'data:image/jpeg;charset=utf-8;base64,'
const SRC_PREFIX = "data:image/jpeg;charset=utf-8;base64,"
export const useImages = (sha: string) => {
const store = useUserRepoStore()
@@ -23,14 +23,14 @@ export const useImages = (sha: string) => {
const images = document.querySelectorAll(`.note-${sha} img`)
images.forEach(async (image) => {
const src = image.getAttribute('src')
const src = image.getAttribute("src")
if (!src || src.startsWith(SRC_PREFIX)) {
return
}
const imageFilePath = resolvePath(
filePath,
image.getAttribute('src') ?? ''
image.getAttribute("src") ?? ""
)
const imageFile = store.files.find(
@@ -43,7 +43,7 @@ export const useImages = (sha: string) => {
const { getCachedFileContent } = useFile(imageFile.sha, false)
const fileContent = await getCachedFileContent()
image.setAttribute('src', `${SRC_PREFIX} ${fileContent}`)
image.setAttribute("src", `${SRC_PREFIX} ${fileContent}`)
})
},
{ immediate: true }

View File

@@ -6,7 +6,7 @@ import { isExternalLink } from "@/utils/link"
export const useLinks = (
className: ComputedRef<string> | string,
sha?: Ref<string> | string,
sha?: Ref<string> | string
) => {
const store = useUserRepoStore()
@@ -34,7 +34,7 @@ export const useLinks = (
path: href,
currentNoteSHA: toValue(sha),
user: store.user,
repo: store.repo,
repo: store.repo
})
}
@@ -74,6 +74,6 @@ export const useLinks = (
})
return {
listenToClick,
listenToClick
}
}

View File

@@ -1,17 +1,18 @@
import markdownItKatex from "@vscode/markdown-it-katex"
import MarkdownIt, { Options } from "markdown-it"
import Renderer, { type RenderRuleRecord } from "markdown-it/lib/renderer.mjs"
import type Token from "markdown-it/lib/token.mjs"
import blockEmbedPlugin from "markdown-it-block-embed"
import markdownItCheckbox from "markdown-it-checkbox"
import MarkdownItGitHubAlerts from "markdown-it-github-alerts"
import markdownItIframe from "markdown-it-iframe"
import Shikiji from "markdown-it-shikiji"
import mermaid from "mermaid"
import { Ref, toValue } from "vue"
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
import { html5Media } from "@/utils/markdown/markdown-html5-media"
import mermaid from "mermaid"
import type Token from "markdown-it/lib/token.mjs"
import Renderer, { type RenderRuleRecord } from "markdown-it/lib/renderer.mjs"
import { markdownItTablerIcons } from "@/utils/markdown/markdown-it-tabler-icons"
const markdownItMermaidExtractor = (md: MarkdownIt) => {
const defaultFence =
@@ -21,7 +22,7 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => {
index: number,
options: Options,
_: unknown,
self: Renderer,
self: Renderer
) {
return self.renderToken(tokens, index, options)
}
@@ -31,7 +32,7 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => {
index: number,
options: Options,
env: unknown,
self: Renderer,
self: Renderer
) {
const token = tokens[index]
@@ -46,22 +47,23 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => {
const md = new MarkdownIt({
typographer: true,
quotes: ["«\xA0", "\xA0»", "\xA0", "\xA0"],
quotes: ["«\xA0", "\xA0»", "\xA0", "\xA0"]
})
.use(markdownItMermaidExtractor)
.use(html5Media)
.use(blockEmbedPlugin, {
youtube: {
width: "100%",
height: 300,
},
height: 300
}
})
.use(markdownItCheckbox)
.use(markdownItKatex)
.use(markdownItIframe, {
width: "100%",
width: "100%"
})
.use(MarkdownItGitHubAlerts)
.use(markdownItTablerIcons)
let shikijiInitialized = false
@@ -75,7 +77,7 @@ export const useShikiji = async () => {
await Shikiji({
themes: {
light: "vitesse-light",
dark: "vitesse-black",
dark: "vitesse-black"
},
langs: [
"bash",
@@ -85,9 +87,9 @@ export const useShikiji = async () => {
"mermaid",
"html",
"css",
"json",
],
}),
"json"
]
})
)
}
@@ -99,19 +101,19 @@ export const runMermaid = (querySelector: string) => {
mermaid.initialize({
theme: "dark",
startOnLoad: false,
flowchart: { curve: "natural" },
flowchart: { curve: "natural" }
})
}
mermaid.run({
querySelector,
querySelector
})
}
const rules: RenderRuleRecord = {
table_open: () =>
'<div class="overflow-x-auto"><table class="table table-zebra">',
table_close: () => "</table></div>",
table_close: () => "</table></div>"
}
md.renderer.rules = { ...md.renderer.rules, ...rules }
@@ -126,7 +128,7 @@ export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
const renderFromUTF8 = (content: string, prefix?: string) => {
return content
? md.render(stripFrontmatter(content), {
docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? ""),
docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? "")
})
: ""
}
@@ -137,6 +139,6 @@ export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
render: (content: string, prefix?: string) =>
renderFromUTF8(decodeBase64ToUTF8(content), prefix),
renderFromUTF8,
getRawContent,
getRawContent
}
}

View File

@@ -1,13 +1,16 @@
import { computed, onMounted, Ref, ref, toValue } from "vue"
import { BOOKMARK_WIDTH_REM, getBookmarkWidthPx } from "@/constants/bookmark-width"
import {
BOOKMARK_WIDTH_REM,
getBookmarkWidthPx
} from "@/constants/bookmark-width"
import { getNoteWidth } from "@/constants/note-width"
import { useOverlay } from "@/hooks/useOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
export const useNoteOverlay = (
className: string,
index: Ref<number> | number,
index: Ref<number> | number
) => {
const { x, y, isMobile } = useOverlay()
const noteHeight = ref(0)
@@ -18,14 +21,17 @@ export const useNoteOverlay = (
if (isMobile.value) {
return y.value > valueIndex * noteHeight.value
} else {
return x.value > valueIndex * getNoteWidth() - valueIndex * getBookmarkWidthPx()
return (
x.value >
valueIndex * getNoteWidth() - valueIndex * getBookmarkWidthPx()
)
}
})
onMounted(() => {
const { stackedNotes } = useRouteQueryStackedNotes()
const noteElement = document.querySelector(
`.${className}`,
`.${className}`
) satisfies HTMLElement | null
if (!noteElement) {
@@ -40,7 +46,7 @@ export const useNoteOverlay = (
noteElement.style.left = `${(toValue(index) + 1) * BOOKMARK_WIDTH_REM}rem`
const stackedNoteContainers = document.querySelectorAll(
".stacked-note",
".stacked-note"
) satisfies NodeListOf<HTMLElement>
stackedNoteContainers.forEach((stackedNote, ind) => {
@@ -52,6 +58,6 @@ export const useNoteOverlay = (
})
return {
displayNoteOverlay,
displayNoteOverlay
}
}

View File

@@ -21,13 +21,13 @@ export const useNoteView = () => {
obj[note] = pathToNotePathTitle(filePath)
return obj
}, {}),
}, {})
)
const unsubscribeLink = noteEventBus.addEventBusListener(
({ path, currentNoteSHA }) => {
const currentFile = store.files.find(
(file) => file.sha === currentNoteSHA,
(file) => file.sha === currentNoteSHA
)
const absolutePath = resolvePath(currentFile?.path ?? "", path)
@@ -39,7 +39,7 @@ export const useNoteView = () => {
}
addStackedNote(currentNoteSHA ?? "", file.sha)
},
}
)
onUnmounted(() => {
@@ -47,6 +47,6 @@ export const useNoteView = () => {
})
return {
titles,
titles
}
}

View File

@@ -1,12 +1,12 @@
import { useAsyncState } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useAsyncState } from "@vueuse/core"
import { computed, ref } from "vue"
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { prepareNoteCache } from '@/modules/note/cache/prepareNoteCache'
import { Note } from '@/modules/note/models/Note'
import { queryFileContent } from '@/modules/repo/services/repo'
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store'
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
import { Note } from "@/modules/note/models/Note"
import { queryFileContent } from "@/modules/repo/services/repo"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
export const useOfflineNotes = () => {
const store = useUserRepoStore()

View File

@@ -19,11 +19,11 @@ export const useOverlay = (listen = true) => {
}
useEventListener(window, "scroll", updateScroll, {
passive: true,
capture: false,
capture: false
})
useEventListener(document.body, "scroll", updateScroll, {
passive: true,
capture: false,
capture: false
})
}
@@ -47,6 +47,6 @@ export const useOverlay = (listen = true) => {
x,
y,
isMobile,
scrollToNote,
scrollToNote
}
}

View File

@@ -1,7 +1,8 @@
import { computedAsync } from "@vueuse/core"
import { computed, Ref, ref } from "vue"
import { Author, getAuthors } from "@/modules/atproto/getAuthor"
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref } from "vue"
interface UsePublicNoteListOptions {
did?: Ref<string | undefined>
@@ -50,6 +51,6 @@ export function usePublicNoteList(options?: UsePublicNoteListOptions) {
canLoadMore,
onLoadMore,
authors,
getAuthor,
getAuthor
}
}

View File

@@ -1,8 +1,8 @@
import { useAsyncState } from '@vueuse/core'
import { useAsyncState } from "@vueuse/core"
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook'
import { RepoBase } from '@/modules/repo/interfaces/RepoBase'
import { getOctokit } from '@/modules/repo/services/octo'
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
import { getOctokit } from "@/modules/repo/services/octo"
export const useRepos = () => {
const { username, accessToken } = useGitHubLogin()
@@ -13,7 +13,7 @@ export const useRepos = () => {
const octokit = await getOctokit()
const repoList = await octokit.request('GET /search/repositories', {
const repoList = await octokit.request("GET /search/repositories", {
q: `user:${username.value}`,
per_page: 100
})

View File

@@ -1,17 +1,17 @@
import { onMounted, watch, type Ref } from "vue"
import { onMounted, type Ref, watch } from "vue"
import { getNoteWidth } from "@/constants/note-width"
import { useOverlay } from "@/hooks/useOverlay.hook"
export const useResizeContainer = (
containerClass: string,
stackedNotes: Readonly<Ref<readonly string[]>>,
stackedNotes: Readonly<Ref<readonly string[]>>
) => {
const { isMobile } = useOverlay(false)
const resizeContainer = () => {
const container = document.querySelector(
`.${containerClass}`,
`.${containerClass}`
) as HTMLElement | null
if (!container) {
@@ -32,6 +32,6 @@ export const useResizeContainer = (
})
watch(stackedNotes, resizeContainer, {
immediate: true,
immediate: true
})
}

View File

@@ -14,7 +14,7 @@ export const useRouteQueryStackedNotes = () => {
}
return Array.isArray(value) ? value : [value]
},
}
})
const { height } = useWindowSize()
@@ -22,7 +22,7 @@ export const useRouteQueryStackedNotes = () => {
const scrollToFocusedNote = (
noteId: string | null = null,
notes: string[] = stackedNotes.value,
notes: string[] = stackedNotes.value
) => {
nextTick(() => {
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
@@ -31,7 +31,7 @@ export const useRouteQueryStackedNotes = () => {
if (noteId) {
const cleanNoteId = noteId.replaceAll(":", "-")
const element = document.querySelector(
`.note-${cleanNoteId}`,
`.note-${cleanNoteId}`
) as HTMLElement
const top = (index + 1) * (element?.clientHeight ?? height.value)
@@ -53,7 +53,7 @@ export const useRouteQueryStackedNotes = () => {
const addStackedNote = (
currentSha: string,
sha: string,
selector?: string,
selector?: string
) => {
if (stackedNotes.value.includes(sha)) {
scrollToFocusedNote(selector ?? sha)
@@ -70,7 +70,7 @@ export const useRouteQueryStackedNotes = () => {
const newStackedNotes = [
...splittedStackedNotes.replaceAll(";;", ";").split(";"),
currentSha,
sha,
sha
].filter((sha) => !!sha)
stackedNotes.value = newStackedNotes
@@ -82,6 +82,6 @@ export const useRouteQueryStackedNotes = () => {
return {
stackedNotes: readonly(stackedNotes),
addStackedNote,
scrollToFocusedNote,
scrollToFocusedNote
}
}

View File

@@ -1,19 +1,19 @@
import { useTitle } from '@vueuse/core'
import { computed, Ref, toValue, watch } from 'vue'
import { useTitle } from "@vueuse/core"
import { computed, Ref, toValue, watch } from "vue"
import { useRouteQueryStackedNotes } from '@/hooks/useRouteQueryStackedNotes.hook'
import { useNotes } from '@/modules/note/hooks/useNotes'
import { pathToNoteTitle } from '@/utils/noteTitle'
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { useNotes } from "@/modules/note/hooks/useNotes"
import { pathToNoteTitle } from "@/utils/noteTitle"
export const generateTitle = (titles: string[]) => titles.join(' | ')
export const generateTitle = (titles: string[]) => titles.join(" | ")
export const useTitleNotes = (prefix: Ref<string> | string) => {
const { stackedNotes } = useRouteQueryStackedNotes()
const { notes } = useNotes()
const titleNotes = computed(() =>
notes.value
.filter((note) => stackedNotes.value.includes(note.sha ?? ''))
.map((note) => pathToNoteTitle(note.path ?? ''))
.filter((note) => stackedNotes.value.includes(note.sha ?? ""))
.map((note) => pathToNoteTitle(note.path ?? ""))
)
const title = useTitle(generateTitle([toValue(prefix), ...titleNotes.value]))

View File

@@ -1,5 +1,5 @@
import en from './en.json'
import fr from './fr.json'
import en from "./en.json"
import fr from "./fr.json"
export const messages = {
en,

View File

@@ -1,5 +1,6 @@
import "notyf/notyf.min.css"
import "./styles/app.css"
import "@/analytics/openpanel"
import { VueQueryPlugin } from "@tanstack/vue-query"
import { createPinia } from "pinia"
@@ -13,7 +14,7 @@ import App from "./App.vue"
const i18n = createI18n({
locale: "en",
messages,
messages
})
createApp(App)

View File

@@ -1,4 +1,4 @@
import { createSchema, createFetch } from "@better-fetch/fetch"
import { createFetch, createSchema } from "@better-fetch/fetch"
import { type } from "arktype"
export type Author = { handle: string; pds: string }
@@ -12,20 +12,20 @@ const schema = createSchema(
did: "string",
handle: "string",
pds: "string",
signing_key: "string",
signing_key: "string"
}),
query: type({
identifier: "string",
}),
identifier: "string"
})
}
},
},
{ strict: true },
{ strict: true }
)
const microcosmSlingshot = createFetch({
baseURL: "https://slingshot.microcosm.blue",
// plugins: [logger()],
schema,
schema
})
export const getAuthor = async (did: string): Promise<Author | null> => {
@@ -36,7 +36,7 @@ export const getAuthor = async (did: string): Promise<Author | null> => {
try {
const { data: author } = await microcosmSlingshot(
"/xrpc/blue.microcosm.identity.resolveMiniDoc",
{ query: { identifier: did } },
{ query: { identifier: did } }
)
if (!author) {
@@ -62,7 +62,7 @@ export const getAuthors = async (dids: Set<string>) => {
const { data: author } = await microcosmSlingshot(
"/xrpc/blue.microcosm.identity.resolveMiniDoc",
{ query: { identifier: did } },
{ query: { identifier: did } }
)
if (!author) {
@@ -72,7 +72,7 @@ export const getAuthors = async (dids: Set<string>) => {
correspondanceCache.set(did, author)
return [did, author] as [string, Author | null]
}),
})
)
return new Map(correspondance)

View File

@@ -1,6 +1,6 @@
import {
BrowserOAuthClient,
buildLoopbackClientId,
buildLoopbackClientId
} from "@atproto/oauth-client-browser"
const getClientId = () =>
@@ -14,7 +14,7 @@ export const getOAuthClient = (): Promise<BrowserOAuthClient> => {
if (!clientPromise) {
clientPromise = BrowserOAuthClient.load({
clientId: getClientId(),
handleResolver: "https://bsky.social",
handleResolver: "https://bsky.social"
})
}
return clientPromise

View File

@@ -1,6 +1,6 @@
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { AtprotoSession } from '@/data/models/AtprotoSession'
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { AtprotoSession } from "@/data/models/AtprotoSession"
const SESSION_ID = `${DataType.AtprotoSession}-current`
@@ -8,12 +8,15 @@ export const loadSession = (): Promise<AtprotoSession | null> => {
return data.get<DataType.AtprotoSession, AtprotoSession>(SESSION_ID)
}
export const saveSession = async (did: string, handle: string): Promise<void> => {
export const saveSession = async (
did: string,
handle: string
): Promise<void> => {
const session: AtprotoSession = {
_id: SESSION_ID,
$type: DataType.AtprotoSession,
did,
handle,
handle
}
await data.update<DataType.AtprotoSession, AtprotoSession>(session)
}

View File

@@ -3,15 +3,18 @@ export const getFollows = async (did: string): Promise<Set<string>> => {
let cursor: string | undefined
do {
const url = new URL('https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows')
url.searchParams.set('actor', did)
url.searchParams.set('limit', '100')
const url = new URL(
"https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows"
)
url.searchParams.set("actor", did)
url.searchParams.set("limit", "100")
if (cursor) {
url.searchParams.set('cursor', cursor)
url.searchParams.set("cursor", cursor)
}
const response = await fetch(url)
const result: { follows: { did: string }[]; cursor?: string } = await response.json()
const result: { follows: { did: string }[]; cursor?: string } =
await response.json()
for (const follow of result.follows) {
follows.add(follow.did)

View File

@@ -1,6 +1,6 @@
export const withATProtoImages = (
markdown: string,
{ pds, did }: { pds: string; did: string },
{ pds, did }: { pds: string; did: string }
): string => {
const imageLinkPattern = /!\[([^\]]*)\]\((bafkrei[a-z0-9]+)\)/g

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { computed, ref } from "vue"
import FlipCard from '@/modules/card/components/FlipCard.vue'
import { Repetition } from '@/modules/card/hooks/useSpacedRepetitionCards'
import FlipCard from "@/modules/card/components/FlipCard.vue"
import { Repetition } from "@/modules/card/hooks/useSpacedRepetitionCards"
const props = defineProps<{ cards: Repetition[] }>()
const emits = defineEmits<{
@@ -22,24 +22,24 @@ const sortedCards = ref(
const currentIndex = ref(0)
const goToNextCard = (success: boolean) => {
const id = sortedCards.value[currentIndex.value].repetition._id ?? ''
const id = sortedCards.value[currentIndex.value].repetition._id ?? ""
if (success) {
emits('success', id)
emits("success", id)
} else {
const failedCard = sortedCards.value.at(currentIndex.value)
if (failedCard) {
sortedCards.value.push(failedCard)
}
emits('fail', id)
emits("fail", id)
}
currentIndex.value++
}
const needsReview = () => {
const id = sortedCards.value[currentIndex.value].repetition._id ?? ''
emits('needsReview', id)
const id = sortedCards.value[currentIndex.value].repetition._id ?? ""
emits("needsReview", id)
currentIndex.value++
}
</script>

View File

@@ -1,8 +1,8 @@
import { useAsyncState } from '@vueuse/core'
import { useAsyncState } from "@vueuse/core"
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { RepetitionCard } from '@/modules/card/models/RepetitionCard'
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { RepetitionCard } from "@/modules/card/models/RepetitionCard"
export const useNeedReviewCards = () => {
const { state: cardsToReview, isReady } = useAsyncState(async () => {

View File

@@ -31,14 +31,14 @@ export const useSpacedRepetitionCards = () => {
(file) =>
file.path !== undefined &&
file.path.startsWith("_cards") &&
file.path.endsWith(".md"),
),
file.path.endsWith(".md")
)
)
const {
state: cards,
isReady,
execute,
execute
} = useAsyncState(
async () => {
const cards: Repetition[] = []
@@ -55,7 +55,7 @@ export const useSpacedRepetitionCards = () => {
$type: DataType.RepetitionCard,
level: 1,
repeatDate: new Date(),
needsReview: false,
needsReview: false
})
if (
@@ -77,20 +77,20 @@ export const useSpacedRepetitionCards = () => {
card: {
front: toHTML(front),
back: toHTML(back),
references: toHTML(references),
},
references: toHTML(references)
}
})
}
return cards
},
[],
{ immediate: false },
{ immediate: false }
)
const successRepetition = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId,
cardId
)
if (!repetition) {
return
@@ -100,13 +100,13 @@ export const useSpacedRepetitionCards = () => {
...repetition,
needsReview: false,
level: Math.min(repetition.level + 1, MAX_LEVEL),
repeatDate: addDays(new Date(), 2 ** repetition.level),
repeatDate: addDays(new Date(), 2 ** repetition.level)
})
}
const failRepetition = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId,
cardId
)
if (!repetition) {
return
@@ -118,13 +118,13 @@ export const useSpacedRepetitionCards = () => {
...repetition,
level,
needsReview: false,
repeatDate: addDays(new Date(), level),
repeatDate: addDays(new Date(), level)
})
}
const needsReview = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId,
cardId
)
if (!repetition) {
return
@@ -132,7 +132,7 @@ export const useSpacedRepetitionCards = () => {
await data.update<DataType.RepetitionCard, RepetitionCard>({
...repetition,
needsReview: true,
needsReview: true
})
}
@@ -142,7 +142,7 @@ export const useSpacedRepetitionCards = () => {
nextTick(() => {
listenToClick()
}),
{ immediate: true },
{ immediate: true }
)
watch(cardFiles, () => execute())
@@ -152,6 +152,6 @@ export const useSpacedRepetitionCards = () => {
successRepetition,
failRepetition,
needsReview,
isLoading: !isReady,
isLoading: !isReady
}
}

View File

@@ -1,5 +1,5 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
import { DataType } from "@/data/DataType.enum"
import { Model } from "@/data/models/Model"
export interface RepetitionCard extends Model<DataType.RepetitionCard> {
level: number

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { useLastVisitedRepos } from '@/modules/history/hooks/useLastVisitedRepos.hook'
import { useLastVisitedRepos } from "@/modules/history/hooks/useLastVisitedRepos.hook"
const { lastVisitedRepos } = useLastVisitedRepos()
</script>

View File

@@ -1,17 +1,17 @@
import { useAsyncState } from '@vueuse/core'
import { computed } from 'vue'
import { useAsyncState } from "@vueuse/core"
import { computed } from "vue"
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { History } from '@/data/models/History'
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { History } from "@/data/models/History"
const HISTORY_ID = data.generateId(DataType.History, 'history')
const HISTORY_ID = data.generateId(DataType.History, "history")
export const useLastVisitedRepos = () => {
const history = useAsyncState(
() =>
data.get<DataType.History, History>(
data.generateId(DataType.History, 'history')
data.generateId(DataType.History, "history")
),
null
)

View File

@@ -1,10 +1,10 @@
import { Ref, toValue } from 'vue'
import { Ref, toValue } from "vue"
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { History } from '@/data/models/History'
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { History } from "@/data/models/History"
const HISTORY_ID = data.generateId(DataType.History, 'history')
const HISTORY_ID = data.generateId(DataType.History, "history")
const MAX_REPO_HISTORY = 10
export const useVisitRepo = (newRepo: {

View File

@@ -1,14 +1,14 @@
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { Note } from '@/modules/note/models/Note'
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store'
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { Note } from "@/modules/note/models/Note"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
type NoteCacheResult =
| {
note: Note
from: 'sha'
from: "sha"
}
| { note: Note; from: 'path' }
| { note: Note; from: "path" }
| { note: null; from: null }
export const prepareNoteCache = (sha: string, path?: string) => {
@@ -20,7 +20,7 @@ export const prepareNoteCache = (sha: string, path?: string) => {
const note = await data.get<DataType.Note, Note>(noteId)
if (note) {
return { note, from: 'sha' }
return { note, from: "sha" }
}
if (notePath) {
@@ -33,7 +33,7 @@ export const prepareNoteCache = (sha: string, path?: string) => {
}
return {
note,
from: 'path'
from: "path"
}
}

View File

@@ -1,4 +1,5 @@
import { computed } from "vue"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
export const useFolderNotes = (folders: string[]) => {
@@ -8,8 +9,8 @@ export const useFolderNotes = (folders: string[]) => {
store.files.filter(
(file) =>
folders.some((folder) => file.path?.startsWith(folder)) &&
file.path?.endsWith(".md"),
),
file.path?.endsWith(".md")
)
)
const content = computed(() =>
@@ -23,10 +24,10 @@ export const useFolderNotes = (folders: string[]) => {
})`
})
.join("\n")
: "",
: ""
)
return {
content,
content
}
}

View File

@@ -6,10 +6,10 @@ export const useNotes = () => {
const store = useUserRepoStore()
const notes = computed(() =>
store.files.filter((file) => file.path?.endsWith(".md")),
store.files.filter((file) => file.path?.endsWith(".md"))
)
return {
notes,
notes
}
}

View File

@@ -1,6 +1,6 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
import { Backlink } from '@/modules/note/models/Backlink'
import { DataType } from "@/data/DataType.enum"
import { Model } from "@/data/models/Model"
import { Backlink } from "@/modules/note/models/Backlink"
export interface BacklinkNote extends Model<DataType.BacklinkNote> {
sha: string

View File

@@ -1,13 +1,13 @@
import { initContract } from "@ts-rest/core"
import { type } from "arktype"
import { initQueryClient } from "@ts-rest/vue-query"
import { type } from "arktype"
const PublicNoteListItem = type({
did: "string",
rkey: "string",
title: "string",
publishedAt: "string",
createdAt: "string",
createdAt: "string"
})
export type PublicNoteListItem = typeof PublicNoteListItem.infer
@@ -18,7 +18,7 @@ const PublicNote = type({
title: "string",
content: "string",
publishedAt: "string",
createdAt: "string",
createdAt: "string"
})
export type PublicNote = typeof PublicNote.infer
@@ -31,34 +31,34 @@ export const noteRouter = contract.router({
path: "/notes",
query: type({
cursor: "string | undefined",
limit: "number | undefined",
limit: "number | undefined"
}),
responses: {
200: type({
notes: PublicNoteListItem.array(),
}),
notes: PublicNoteListItem.array()
})
},
summary: "List all notes",
summary: "List all notes"
},
noteListsByDid: {
method: "GET",
path: "/:did/notes",
pathParams: type({
did: "string",
did: "string"
}),
query: type({
cursor: "string | undefined",
limit: "number | undefined",
limit: "number | undefined"
}),
responses: {
200: type({
notes: PublicNoteListItem.array(),
}),
},
summary: "List all notes",
notes: PublicNoteListItem.array()
})
},
summary: "List all notes"
}
})
export const client = initQueryClient(noteRouter, {
baseUrl: "https://api.remanso.space",
baseUrl: "https://api.remanso.space"
})

View File

@@ -1,10 +1,10 @@
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref } from "vue"
import { data } from '@/data/data'
import { DataType } from '@/data/DataType.enum'
import { useRepos } from '@/hooks/useRepos.hook'
import { RepoBase } from '@/modules/repo/interfaces/RepoBase'
import { FavoriteRepo } from '@/modules/repo/models/FavoriteRepo'
import { data } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { useRepos } from "@/hooks/useRepos.hook"
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
import { FavoriteRepo } from "@/modules/repo/models/FavoriteRepo"
export const useFavoriteRepos = () => {
const { repos } = useRepos()

View File

@@ -1,8 +1,8 @@
import { computed } from 'vue'
import { computed } from "vue"
import { useRepos } from '@/hooks/useRepos.hook'
import { useFavoriteRepos } from '@/modules/repo/hooks/useFavoriteRepos.hook'
import { RepoBase } from '@/modules/repo/interfaces/RepoBase'
import { useRepos } from "@/hooks/useRepos.hook"
import { useFavoriteRepos } from "@/modules/repo/hooks/useFavoriteRepos.hook"
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
export const useRepoList = () => {
const { savedFavoriteRepos, addFavorite, removeFavorite } = useFavoriteRepos()

View File

@@ -1,5 +1,5 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
import { DataType } from "@/data/DataType.enum"
import { Model } from "@/data/models/Model"
export interface FavoriteRepo extends Model<DataType.FavoriteRepo> {
isFavorite: boolean

View File

@@ -1,6 +1,6 @@
import { DataType } from '@/data/DataType.enum'
import { Model } from '@/data/models/Model'
import { RepoFile } from '@/modules/repo/interfaces/RepoFile'
import { DataType } from "@/data/DataType.enum"
import { Model } from "@/data/models/Model"
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
export interface SavedRepo extends Model<DataType.SavedRepo> {
user: string

View File

@@ -1,11 +1,11 @@
import { Octokit } from '@octokit/rest'
import { Octokit } from "@octokit/rest"
import { getAccessToken } from '@/modules/user/service/signIn'
import { getAccessToken } from "@/modules/user/service/signIn"
export const getOctokit = async (): Promise<Octokit> => {
const response = await getAccessToken()
return new Octokit({
auth: response?.token ?? ''
auth: response?.token ?? ""
})
}

View File

@@ -6,7 +6,7 @@ import { getOctokit } from "@/modules/repo/services/octo"
export const getFiles = async (
owner: string,
repo: string,
repo: string
): Promise<RepoFile[]> => {
if (!owner || !repo) {
return []
@@ -15,7 +15,7 @@ export const getFiles = async (
const commits = await octokit.request("GET /repos/{owner}/{repo}/commits", {
owner,
repo,
repo
})
const lastCommit = commits.data.shift()
@@ -30,8 +30,8 @@ export const getFiles = async (
owner,
repo,
tree_sha: lastCommit.commit.tree.sha,
recursive: "true",
},
recursive: "true"
}
)
return treeResponse?.data.tree.filter((t) => t.type === "blob") ?? []
@@ -60,7 +60,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
const { render } = markdownBuilder()
const { getCachedNote, saveCacheNote } = prepareNoteCache(
`${owner}-${repo}-README`,
`${owner}-${repo}-README`
)
try {
@@ -68,7 +68,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
const README = await octokit.repos.getReadme({
owner,
repo,
repo
})
if (README) {
@@ -90,7 +90,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
export const getUserSettingsContent = async (
user: string,
repo: string,
files: RepoFile[],
files: RepoFile[]
): Promise<Omit<UserSettings, "chosenFontFamily"> | null> => {
const configFile = files.find((file) => file.path === ".remanso.json")
@@ -110,7 +110,7 @@ export const getUserSettingsContent = async (
export const queryFileContent = async (
user: string,
repo: string,
sha: string,
sha: string
) => {
const octokit = await getOctokit()
@@ -123,8 +123,8 @@ export const queryFileContent = async (
{
owner: user,
repo: repo,
file_sha: sha,
},
file_sha: sha
}
)
return file?.data.content ?? null

View File

@@ -1,43 +1,43 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it } from "vitest"
import { resolvePath } from './resolvePath'
import { resolvePath } from "./resolvePath"
describe('resolve path service', () => {
it('set the absolute path if path to resolve is empty', () => {
expect(resolvePath('standard/README.md', '')).toEqual('standard/')
describe("resolve path service", () => {
it("set the absolute path if path to resolve is empty", () => {
expect(resolvePath("standard/README.md", "")).toEqual("standard/")
})
it('returns the path sanitized if there is no absolute path', () => {
expect(resolvePath('', './here/note.md')).toEqual('here/note.md')
it("returns the path sanitized if there is no absolute path", () => {
expect(resolvePath("", "./here/note.md")).toEqual("here/note.md")
})
it('set the absolute path from the current path', () => {
expect(resolvePath('standard/README.md', './other-note.md')).toEqual(
'standard/other-note.md'
it("set the absolute path from the current path", () => {
expect(resolvePath("standard/README.md", "./other-note.md")).toEqual(
"standard/other-note.md"
)
})
it('set the absolute path from the current path with multiple level', () => {
it("set the absolute path from the current path with multiple level", () => {
expect(
resolvePath('standard/you/are/here/README.md', './other-note.md')
).toEqual('standard/you/are/here/other-note.md')
resolvePath("standard/you/are/here/README.md", "./other-note.md")
).toEqual("standard/you/are/here/other-note.md")
})
it('set the absolute path from the current path with a go back in the relative path', () => {
it("set the absolute path from the current path with a go back in the relative path", () => {
expect(
resolvePath('standard/you/are/here/README.md', '../other-note.md')
).toEqual('standard/you/are/other-note.md')
resolvePath("standard/you/are/here/README.md", "../other-note.md")
).toEqual("standard/you/are/other-note.md")
expect(
resolvePath('standard/you/are/here/README.md', '../../other-note.md')
).toEqual('standard/you/other-note.md')
resolvePath("standard/you/are/here/README.md", "../../other-note.md")
).toEqual("standard/you/other-note.md")
expect(
resolvePath('standard/you/are/here/README.md', './../../other-note.md')
).toEqual('standard/you/other-note.md')
resolvePath("standard/you/are/here/README.md", "./../../other-note.md")
).toEqual("standard/you/other-note.md")
expect(
resolvePath('standard/you/are/here/README.md', './../../../other-note.md')
).toEqual('standard/other-note.md')
resolvePath("standard/you/are/here/README.md", "./../../../other-note.md")
).toEqual("standard/other-note.md")
})
})

View File

@@ -1,15 +1,15 @@
const sanitizePath = (path: string) => {
if (path.startsWith('./')) {
return decodeURIComponent(path.replace('./', ''))
if (path.startsWith("./")) {
return decodeURIComponent(path.replace("./", ""))
}
return decodeURIComponent(path)
}
const removeNoteFilename = (pathNote: string) => {
const path = pathNote.split('/')
const path = pathNote.split("/")
path.pop()
return sanitizePath(path.join('/'))
return sanitizePath(path.join("/"))
}
export const resolvePath = (
@@ -19,11 +19,11 @@ export const resolvePath = (
let currentAbsolutePath = removeNoteFilename(currentAbsolutePathNote)
pathToResolve = sanitizePath(pathToResolve)
while (pathToResolve.startsWith('../')) {
const adjustedAbsolutePath = currentAbsolutePath.split('/')
while (pathToResolve.startsWith("../")) {
const adjustedAbsolutePath = currentAbsolutePath.split("/")
adjustedAbsolutePath.pop()
currentAbsolutePath = adjustedAbsolutePath.join('/')
pathToResolve = pathToResolve.replace('../', '')
currentAbsolutePath = adjustedAbsolutePath.join("/")
pathToResolve = pathToResolve.replace("../", "")
}
return currentAbsolutePath

Some files were not shown because too many files have changed in this diff Show More