Compare commits

...

27 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
Julien Calixte
514d08946d Merge branch 'main' of ssh://git.apoena.dev:22222/julien/remanso 2026-03-17 22:03:33 +01:00
Julien Calixte
16efd8c637 design: better header and subheader 2026-03-16 23:22:25 +01:00
127 changed files with 2464 additions and 1319 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 │ ├── card/ # Spaced repetition
│ ├── history/ # Edit history tracking │ ├── history/ # Edit history tracking
│ ├── atproto/ # ATProto/Bluesky integration (DID resolution, blob URLs) │ ├── 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.) ├── hooks/ # Composition hooks (useMarkdown, useBacklinks, useGitHubContent, etc.)
├── data/ # PouchDB wrapper and data models ├── data/ # PouchDB wrapper and data models
├── utils/ # Utilities including custom markdown-it plugins ├── 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 { readFileSync, writeFileSync } from "fs"
import { join } from "path" import { join } from "path"
import { commitTheme } from "./change-theme" import { commitTheme } from "./change-theme"
// Chemins vers les fichiers // Chemins vers les fichiers
@@ -28,7 +29,7 @@ let themeConfigContent = readFileSync(themeConfigPath, "utf8")
// Remplacer la valeur du thème sombre // Remplacer la valeur du thème sombre
themeConfigContent = themeConfigContent.replace( themeConfigContent = themeConfigContent.replace(
/dark:\s*['"][^'"]*['"],/, /dark:\s*['"][^'"]*['"],/,
`dark: '${newTheme}',`, `dark: '${newTheme}',`
) )
// Écrire le contenu mis à jour dans le fichier // Écrire le contenu mis à jour dans le fichier
@@ -38,7 +39,7 @@ writeFileSync(themeConfigPath, themeConfigContent)
let appCssContent = readFileSync(appCssPath, "utf8") let appCssContent = readFileSync(appCssPath, "utf8")
appCssContent = appCssContent.replace( appCssContent = appCssContent.replace(
/(\s+)([a-zA-Z0-9-]+)(\s+--prefersdark;)/, /(\s+)([a-zA-Z0-9-]+)(\s+--prefersdark;)/,
`$1${newTheme}$3`, `$1${newTheme}$3`
) )
writeFileSync(appCssPath, appCssContent) writeFileSync(appCssPath, appCssContent)

View File

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

View File

@@ -1,3 +1,3 @@
module.exports = { 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" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/4.0.0/github-markdown.min.css" 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> </head>
<body> <body>
<noscript> <noscript>

View File

@@ -9,9 +9,9 @@ status = 200
[[headers]] [[headers]]
for = "/client-metadata.json" for = "/client-metadata.json"
[headers.values] [headers.values]
Access-Control-Allow-Origin = "*" Access-Control-Allow-Origin = "*"
Content-Type = "application/json" Content-Type = "application/json"
[[redirects]] [[redirects]]
from = "/client-metadata.json" 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", "serve": "vite preview",
"test": "vitest", "test": "vitest",
"types": "tsc --noEmit", "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", "prepare": "husky",
"theme:light": "esno _scripts/change-theme-light.ts", "theme:light": "esno _scripts/change-theme-light.ts",
"theme:dark": "esno _scripts/change-theme-dark.ts", "theme:dark": "esno _scripts/change-theme-dark.ts",
@@ -21,6 +24,7 @@
"@intlify/unplugin-vue-i18n": "^6.0.8", "@intlify/unplugin-vue-i18n": "^6.0.8",
"@octokit/core": "^7.0.6", "@octokit/core": "^7.0.6",
"@octokit/rest": "^22.0.1", "@octokit/rest": "^22.0.1",
"@openpanel/web": "^1.3.0",
"@tailwindcss/postcss": "^4.1.16", "@tailwindcss/postcss": "^4.1.16",
"@tanstack/vue-query": "^5.92.9", "@tanstack/vue-query": "^5.92.9",
"@toycode/markdown-it-class": "^1.2.4", "@toycode/markdown-it-class": "^1.2.4",
@@ -53,42 +57,38 @@
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-i18n": "^11.1.11", "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": { "devDependencies": {
"@babel/core": "^7.28.5", "@babel/core": "^7.28.5",
"@rushstack/eslint-patch": "^1.14.1",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@types/fontfaceobserver": "^2.1.3", "@types/fontfaceobserver": "^2.1.3",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/node": "^22.15.24", "@types/node": "^22.15.24",
"@types/pouchdb-browser": "^6.1.5", "@types/pouchdb-browser": "^6.1.5",
"@types/sanitize-html": "^2.16.0", "@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", "@vite-pwa/assets-generator": "^1.0.2",
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.28", "@vue/compiler-sfc": "^3.5.28",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"autoprefixer": "^10.4.24", "autoprefixer": "^10.4.24",
"daisyui": "^5.5.18", "daisyui": "^5.5.18",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier-vue": "^5.0.0", "eslint-plugin-prettier-vue": "^5.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"eslint-plugin-vue": "^10.8.0",
"esno": "^4.8.0", "esno": "^4.8.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-vue": "^1.1.2", "prettier-vue": "^1.1.2",
"sass": "^1.93.3", "sass": "^1.98.0",
"tailwindcss": "^4.1.16", "tailwindcss": "^4.2.2",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.1.12", "vite": "^8.0.1",
"vite-plugin-pwa": "^1.1.0", "vite-plugin-pwa": "^1.2.0",
"vitest": "^3.2.4" "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 = { 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_id": "https://remanso.space/client-metadata.json",
"client_name": "Remanso", "client_name": "Remanso",
"client_uri": "https://remanso.space", "client_uri": "https://remanso.space",
"redirect_uris": [ "redirect_uris": ["https://remanso.space/"],
"https://remanso.space/"
],
"scope": "atproto transition:generic", "scope": "atproto transition:generic",
"grant_types": ["authorization_code", "refresh_token"], "grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"], "response_types": ["code"],

View File

@@ -1,9 +1,9 @@
import { import {
defineConfig, defineConfig,
minimal2023Preset as preset, minimal2023Preset as preset
} from "@vite-pwa/assets-generator/config" } from "@vite-pwa/assets-generator/config"
export default defineConfig({ export default defineConfig({
preset, 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> <script lang="ts" setup>
import NewVersion from '@/components/NewVersion.vue' import NewVersion from "@/components/NewVersion.vue"
import { useATProtoLogin } from '@/hooks/useATProtoLogin.hook' import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
const { isReady } = useGitHubLogin() const { isReady } = useGitHubLogin()
const { isATProtoReady } = useATProtoLogin() const { isATProtoReady } = useATProtoLogin()
@@ -18,7 +18,23 @@ const { isATProtoReady } = useATProtoLogin()
<style lang="scss"> <style lang="scss">
#main-app { #main-app {
height: 100vh; height: 100vh;
width: 100%;
display: flex; display: flex;
flex: 1; 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> </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 { interface EventBusParams {
fileSha: string fileSha: string

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import {
nextTick, nextTick,
onMounted, onMounted,
ref, ref,
watch, watch
} from "vue" } from "vue"
import { useEditionMode } from "@/hooks/useEditionMode" import { useEditionMode } from "@/hooks/useEditionMode"
@@ -13,20 +13,20 @@ import { useFile } from "@/hooks/useFile.hook"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook" import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
import { useImages } from "@/hooks/useImages.hook" import { useImages } from "@/hooks/useImages.hook"
import { useLinks } from "@/hooks/useLinks.hook" import { useLinks } from "@/hooks/useLinks.hook"
import { runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook" import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { useTitleNotes } from "@/hooks/useTitleNotes.hook" import { useTitleNotes } from "@/hooks/useTitleNotes.hook"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store" import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8" import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
import { filenameToNoteTitle } from "@/utils/noteTitle" import { filenameToNoteTitle } from "@/utils/noteTitle"
import { runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
const LinkedNotes = defineAsyncComponent( const LinkedNotes = defineAsyncComponent(
() => import("@/components/LinkedNotes.vue"), () => import("@/components/LinkedNotes.vue")
) )
const EditNote = defineAsyncComponent( const EditNote = defineAsyncComponent(
() => import("@/modules/note/components/EditNote.vue"), () => import("@/modules/note/components/EditNote.vue")
) )
const props = defineProps<{ const props = defineProps<{
@@ -50,7 +50,7 @@ const {
rawContent, rawContent,
getRawContent, getRawContent,
saveCacheNote, saveCacheNote,
getEditedSha, getEditedSha
} = useFile(sha) } = useFile(sha)
const initialRawContent = ref<string | null>(null) const initialRawContent = ref<string | null>(null)
const className = computed(() => `stacked-note-${props.index}`) const className = computed(() => `stacked-note-${props.index}`)
@@ -67,7 +67,7 @@ const breadcrumbs = computed(() => displayedTitle.value.split(" / "))
const { updateFile } = useGitHubContent({ const { updateFile } = useGitHubContent({
user: user.value, user: user.value,
repo: repo.value, repo: repo.value
}) })
onMounted(async () => { onMounted(async () => {
@@ -115,7 +115,7 @@ watch(mode, async (newMode) => {
const newSha = await updateFile({ const newSha = await updateFile({
content: rawContent.value, content: rawContent.value,
path: path.value, path: path.value,
sha: editedSha, sha: editedSha
}) })
if (!newSha) { if (!newSha) {
@@ -125,7 +125,7 @@ watch(mode, async (newMode) => {
} }
await saveCacheNote(encodeUTF8ToBase64(rawContent.value), { await saveCacheNote(encodeUTF8ToBase64(rawContent.value), {
editedSha: newSha, editedSha: newSha
}) })
initialRawContent.value = rawContent.value initialRawContent.value = rawContent.value
}) })
@@ -137,7 +137,7 @@ watch(mode, async (newMode) => {
:class="{ :class="{
[className]: true, [className]: true,
overlay: displayNoteOverlay, overlay: displayNoteOverlay,
[`note-${sha}`]: true, [`note-${sha}`]: true
}" }"
> >
<a <a
@@ -159,9 +159,11 @@ watch(mode, async (newMode) => {
<button <button
class="action button is-text is-light" class="action button is-text is-light"
:class="{ 'is-link': mode === 'edit' }" :class="{ 'is-link': mode === 'edit' }"
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
@click="toggleMode" @click="toggleMode"
> >
<svg <svg
v-if="mode === 'read'"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-edit" class="icon icon-tabler icon-tabler-edit"
width="24" width="24"
@@ -182,37 +184,29 @@ watch(mode, async (newMode) => {
/> />
<path d="M16 5l3 3" /> <path d="M16 5l3 3" />
</svg> </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> </button>
<div v-if="mode === 'edit'" class="edit"> <div v-if="mode === 'edit'" class="edit">
<edit-note v-model="rawContent" /> <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>
<div v-if="mode === 'read'" class="note-content" v-html="content"></div> <div v-if="mode === 'read'" class="note-content" v-html="content"></div>
</section> </section>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,21 @@
import { ComputedRef, onUnmounted, Ref, toValue } from "vue" import { ComputedRef, onUnmounted, Ref, toValue } from "vue"
import { isExternalLink } from "@/utils/link"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { parseAtUri } from "@/modules/atproto/parseAtUri" import { parseAtUri } from "@/modules/atproto/parseAtUri"
import { toShortDid } from "@/modules/atproto/shortDid" import { toShortDid } from "@/modules/atproto/shortDid"
import { router } from "@/router/router" import { router } from "@/router/router"
import { isExternalLink } from "@/utils/link"
export const useATProtoLinks = ( export const useATProtoLinks = (
className: ComputedRef<string> | string, 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 linkNote = (event: Event) => {
const target = event.target as HTMLElement const target = event.target as HTMLElement
const href = target.getAttribute("href") const href = target.getAttribute("href")
@@ -33,7 +38,7 @@ export const useATProtoLinks = (
if (href.startsWith(window.location.origin)) { if (href.startsWith(window.location.origin)) {
const { params } = router.resolve( const { params } = router.resolve(
href.replace(window.location.origin, ""), href.replace(window.location.origin, "")
) )
if (!params.shortDid || !params.rkey) { if (!params.shortDid || !params.rkey) {
@@ -44,10 +49,15 @@ export const useATProtoLinks = (
? `${params.shortDid}-${params.rkey}-${params.slug}` ? `${params.shortDid}-${params.rkey}-${params.slug}`
: `${params.shortDid}-${params.rkey}` : `${params.shortDid}-${params.rkey}`
if (noteId === toValue(mainNoteId)) {
scrollToFocusedNote(null)
return
}
addStackedNote( addStackedNote(
toValue(currentAtUri) ?? "", toValue(currentAtUri) ?? "",
noteId, noteId,
`${params.shortDid}-${params.rkey}`, `${params.shortDid}-${params.rkey}`
) )
return return
} }
@@ -56,6 +66,11 @@ export const useATProtoLinks = (
const { did, rkey } = parseAtUri(href) const { did, rkey } = parseAtUri(href)
const noteId = `${toShortDid(did)}-${rkey}` const noteId = `${toShortDid(did)}-${rkey}`
if (noteId === toValue(mainNoteId)) {
scrollToFocusedNote(null)
return
}
addStackedNote(toValue(currentAtUri) ?? "", noteId) addStackedNote(toValue(currentAtUri) ?? "", noteId)
} }
} }
@@ -96,6 +111,6 @@ export const useATProtoLinks = (
}) })
return { 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 { getAuthor } from "@/modules/atproto/getAuthor"
import { restoreSession, sdkSignOut, signInWithHandle } from '@/modules/atproto/service/atprotoOAuth' import {
import { clearSession, loadSession, saveSession } from '@/modules/atproto/service/atprotoSession' restoreSession,
sdkSignOut,
signInWithHandle
} from "@/modules/atproto/service/atprotoOAuth"
import {
clearSession,
loadSession,
saveSession
} from "@/modules/atproto/service/atprotoSession"
const did = ref<string | null>(null) const did = ref<string | null>(null)
const handle = ref<string | null>(null) const handle = ref<string | null>(null)
@@ -10,21 +18,26 @@ const handle = ref<string | null>(null)
let init = true let init = true
const initializeAuth = async () => { 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) { if (session) {
const author = await getAuthor(session.did) const author = await getAuthor(session.did)
const resolvedHandle = author?.handle ?? '' const resolvedHandle = author?.handle ?? ""
did.value = session.did did.value = session.did
handle.value = resolvedHandle handle.value = resolvedHandle
await saveSession(session.did, resolvedHandle) await saveSession(session.did, resolvedHandle)
window.history.replaceState(null, '', window.location.pathname + window.location.search) window.history.replaceState(
} else { null,
const stored = await loadSession() "",
did.value = stored?.did ?? '' window.location.pathname + window.location.search
handle.value = stored?.handle ?? '' )
} }
} }
@@ -46,8 +59,8 @@ export const useATProtoLogin = () => {
await sdkSignOut(did.value) await sdkSignOut(did.value)
} }
await clearSession() await clearSession()
did.value = '' did.value = ""
handle.value = '' handle.value = ""
} }
return { return {
@@ -56,6 +69,6 @@ export const useATProtoLogin = () => {
isLoggedIn, isLoggedIn,
isATProtoReady, isATProtoReady,
signIn, signIn,
signOut, signOut
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,18 +17,18 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
const { const {
render, render,
renderFromUTF8, renderFromUTF8,
getRawContent: getRawContentFromFile, getRawContent: getRawContentFromFile
} = markdownBuilder(shaValue) } = markdownBuilder(shaValue)
const { getCachedNote, saveCacheNote } = prepareNoteCache( const { getCachedNote, saveCacheNote } = prepareNoteCache(
shaValue, shaValue,
toValue(path), toValue(path)
) )
const fromCache = ref(false) const fromCache = ref(false)
const rawContent = ref("") const rawContent = ref("")
const content = computed(() => const content = computed(() =>
rawContent.value ? renderFromUTF8(rawContent.value) : "", rawContent.value ? renderFromUTF8(rawContent.value) : ""
) )
const getEditedSha = async () => { const getEditedSha = async () => {
@@ -55,7 +55,7 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
} }
saveCacheNote(fileContent) saveCacheNote(fileContent)
rawContent.value = getRawContentFromFile(fileContent) rawContent.value = getRawContentFromFile(fileContent)
}, }
) )
} }
@@ -111,6 +111,6 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
getCachedFileContent, getCachedFileContent,
getEditedSha, getEditedSha,
fromCache, 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 { Author, getAuthors } from "@/modules/atproto/getAuthor"
import { PublicNoteListItem } from "@/modules/note/models/Note" import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref, watch } from "vue"
export function useFollowingNoteList( export function useFollowingNoteList(
dids: Ref<Set<string>>, dids: Ref<Set<string>>,
enabled: Ref<boolean>, enabled: Ref<boolean>
) { ) {
const isLoading = ref(false) const isLoading = ref(false)
const notes = ref<PublicNoteListItem[]>([]) const notes = ref<PublicNoteListItem[]>([])
const cursor = ref<string | null | undefined>(null) const cursor = ref<string | null | undefined>(null)
const canLoadMore = computed( const canLoadMore = computed(
() => dids.value.size > 0 && cursor.value !== undefined, () => dids.value.size > 0 && cursor.value !== undefined
) )
const onLoadMore = async () => { const onLoadMore = async () => {
@@ -22,7 +23,7 @@ export function useFollowingNoteList(
const body: { dids: string[]; limit: number; cursor?: string } = { const body: { dids: string[]; limit: number; cursor?: string } = {
dids: [...dids.value], dids: [...dids.value],
limit: 20, limit: 20
} }
if (cursor.value) { if (cursor.value) {
@@ -32,7 +33,7 @@ export function useFollowingNoteList(
const response = await fetch("https://api.remanso.space/notes/feed", { const response = await fetch("https://api.remanso.space/notes/feed", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), body: JSON.stringify(body)
}) })
const data: { notes: PublicNoteListItem[]; cursor?: string } = 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>) => { export const useFollows = (did: Ref<string | null>) => {
const follows = ref<Set<string>>(new Set()) const follows = ref<Set<string>>(new Set())
@@ -14,7 +14,7 @@ export const useFollows = (did: Ref<string | null>) => {
follows.value = new Set() follows.value = new Set()
} }
}, },
{ immediate: true }, { immediate: true }
) )
return { follows } return { follows }

View File

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

View File

@@ -4,7 +4,7 @@ import { confirmMessage, errorMessage } from "@/utils/notif"
export const useGitHubContent = ({ export const useGitHubContent = ({
user, user,
repo, repo
}: { }: {
user: string user: string
repo: string repo: string
@@ -12,7 +12,7 @@ export const useGitHubContent = ({
const putFile = async ({ const putFile = async ({
content, content,
path, path,
sha, sha
}: { }: {
content: string content: string
path: string path: string
@@ -29,8 +29,8 @@ export const useGitHubContent = ({
path, path,
message: `Updating ${path} from Remanso`, message: `Updating ${path} from Remanso`,
content: encodeUTF8ToBase64(content), content: encodeUTF8ToBase64(content),
sha, sha
}, }
) )
confirmMessage("✅ Note saved") confirmMessage("✅ Note saved")
@@ -48,6 +48,6 @@ export const useGitHubContent = ({
updateFile: async (props: { content: string; path: string; sha: string }) => updateFile: async (props: { content: string; path: string; sha: string }) =>
putFile(props), putFile(props),
createFile: async (props: { content: string; path: string }) => 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 { GithubToken } from "@/modules/user/interfaces/GithubToken"
import { getAccessToken, saveAccessToken } from '@/modules/user/service/signIn' import { getAccessToken, saveAccessToken } from "@/modules/user/service/signIn"
import { confirmMessage } from '@/utils/notif' import { confirmMessage } from "@/utils/notif"
const username = ref<string | null>(null) const username = ref<string | null>(null)
const accessToken = ref<string | null>(null) const accessToken = ref<string | null>(null)
@@ -11,8 +11,8 @@ let init = true
const saveAccessTokenToLocal = async () => { const saveAccessTokenToLocal = async () => {
const response = await getAccessToken() const response = await getAccessToken()
username.value = response?.username || '' username.value = response?.username || ""
accessToken.value = response?.token || '' accessToken.value = response?.token || ""
} }
const saveCredentials = async (token: GithubToken): Promise<void> => { 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 { useFile } from "@/hooks/useFile.hook"
import { resolvePath } from '@/modules/repo/services/resolvePath' import { resolvePath } from "@/modules/repo/services/resolvePath"
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store' 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) => { export const useImages = (sha: string) => {
const store = useUserRepoStore() const store = useUserRepoStore()
@@ -23,14 +23,14 @@ export const useImages = (sha: string) => {
const images = document.querySelectorAll(`.note-${sha} img`) const images = document.querySelectorAll(`.note-${sha} img`)
images.forEach(async (image) => { images.forEach(async (image) => {
const src = image.getAttribute('src') const src = image.getAttribute("src")
if (!src || src.startsWith(SRC_PREFIX)) { if (!src || src.startsWith(SRC_PREFIX)) {
return return
} }
const imageFilePath = resolvePath( const imageFilePath = resolvePath(
filePath, filePath,
image.getAttribute('src') ?? '' image.getAttribute("src") ?? ""
) )
const imageFile = store.files.find( const imageFile = store.files.find(
@@ -43,7 +43,7 @@ export const useImages = (sha: string) => {
const { getCachedFileContent } = useFile(imageFile.sha, false) const { getCachedFileContent } = useFile(imageFile.sha, false)
const fileContent = await getCachedFileContent() const fileContent = await getCachedFileContent()
image.setAttribute('src', `${SRC_PREFIX} ${fileContent}`) image.setAttribute("src", `${SRC_PREFIX} ${fileContent}`)
}) })
}, },
{ immediate: true } { immediate: true }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,11 +19,11 @@ export const useOverlay = (listen = true) => {
} }
useEventListener(window, "scroll", updateScroll, { useEventListener(window, "scroll", updateScroll, {
passive: true, passive: true,
capture: false, capture: false
}) })
useEventListener(document.body, "scroll", updateScroll, { useEventListener(document.body, "scroll", updateScroll, {
passive: true, passive: true,
capture: false, capture: false
}) })
} }
@@ -47,6 +47,6 @@ export const useOverlay = (listen = true) => {
x, x,
y, y,
isMobile, 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 { Author, getAuthors } from "@/modules/atproto/getAuthor"
import { PublicNoteListItem } from "@/modules/note/models/Note" import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref } from "vue"
interface UsePublicNoteListOptions { interface UsePublicNoteListOptions {
did?: Ref<string | undefined> did?: Ref<string | undefined>
@@ -50,6 +51,6 @@ export function usePublicNoteList(options?: UsePublicNoteListOptions) {
canLoadMore, canLoadMore,
onLoadMore, onLoadMore,
authors, 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 { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { RepoBase } from '@/modules/repo/interfaces/RepoBase' import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
import { getOctokit } from '@/modules/repo/services/octo' import { getOctokit } from "@/modules/repo/services/octo"
export const useRepos = () => { export const useRepos = () => {
const { username, accessToken } = useGitHubLogin() const { username, accessToken } = useGitHubLogin()
@@ -13,7 +13,7 @@ export const useRepos = () => {
const octokit = await getOctokit() const octokit = await getOctokit()
const repoList = await octokit.request('GET /search/repositories', { const repoList = await octokit.request("GET /search/repositories", {
q: `user:${username.value}`, q: `user:${username.value}`,
per_page: 100 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 { getNoteWidth } from "@/constants/note-width"
import { useOverlay } from "@/hooks/useOverlay.hook" import { useOverlay } from "@/hooks/useOverlay.hook"
export const useResizeContainer = ( export const useResizeContainer = (
containerClass: string, containerClass: string,
stackedNotes: Readonly<Ref<readonly string[]>>, stackedNotes: Readonly<Ref<readonly string[]>>
) => { ) => {
const { isMobile } = useOverlay(false) const { isMobile } = useOverlay(false)
const resizeContainer = () => { const resizeContainer = () => {
const container = document.querySelector( const container = document.querySelector(
`.${containerClass}`, `.${containerClass}`
) as HTMLElement | null ) as HTMLElement | null
if (!container) { if (!container) {
@@ -32,6 +32,6 @@ export const useResizeContainer = (
}) })
watch(stackedNotes, resizeContainer, { watch(stackedNotes, resizeContainer, {
immediate: true, immediate: true
}) })
} }

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import "notyf/notyf.min.css" import "notyf/notyf.min.css"
import "./styles/app.css" import "./styles/app.css"
import "@/analytics/openpanel"
import { VueQueryPlugin } from "@tanstack/vue-query" import { VueQueryPlugin } from "@tanstack/vue-query"
import { createPinia } from "pinia" import { createPinia } from "pinia"
@@ -13,7 +14,7 @@ import App from "./App.vue"
const i18n = createI18n({ const i18n = createI18n({
locale: "en", locale: "en",
messages, messages
}) })
createApp(App) 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" import { type } from "arktype"
export type Author = { handle: string; pds: string } export type Author = { handle: string; pds: string }
@@ -12,20 +12,20 @@ const schema = createSchema(
did: "string", did: "string",
handle: "string", handle: "string",
pds: "string", pds: "string",
signing_key: "string", signing_key: "string"
}), }),
query: type({ query: type({
identifier: "string", identifier: "string"
}), })
}
}, },
}, { strict: true }
{ strict: true },
) )
const microcosmSlingshot = createFetch({ const microcosmSlingshot = createFetch({
baseURL: "https://slingshot.microcosm.blue", baseURL: "https://slingshot.microcosm.blue",
// plugins: [logger()], // plugins: [logger()],
schema, schema
}) })
export const getAuthor = async (did: string): Promise<Author | null> => { export const getAuthor = async (did: string): Promise<Author | null> => {
@@ -36,7 +36,7 @@ export const getAuthor = async (did: string): Promise<Author | null> => {
try { try {
const { data: author } = await microcosmSlingshot( const { data: author } = await microcosmSlingshot(
"/xrpc/blue.microcosm.identity.resolveMiniDoc", "/xrpc/blue.microcosm.identity.resolveMiniDoc",
{ query: { identifier: did } }, { query: { identifier: did } }
) )
if (!author) { if (!author) {
@@ -62,7 +62,7 @@ export const getAuthors = async (dids: Set<string>) => {
const { data: author } = await microcosmSlingshot( const { data: author } = await microcosmSlingshot(
"/xrpc/blue.microcosm.identity.resolveMiniDoc", "/xrpc/blue.microcosm.identity.resolveMiniDoc",
{ query: { identifier: did } }, { query: { identifier: did } }
) )
if (!author) { if (!author) {
@@ -72,7 +72,7 @@ export const getAuthors = async (dids: Set<string>) => {
correspondanceCache.set(did, author) correspondanceCache.set(did, author)
return [did, author] as [string, Author | null] return [did, author] as [string, Author | null]
}), })
) )
return new Map(correspondance) return new Map(correspondance)

View File

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

View File

@@ -1,6 +1,6 @@
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { AtprotoSession } from '@/data/models/AtprotoSession' import { AtprotoSession } from "@/data/models/AtprotoSession"
const SESSION_ID = `${DataType.AtprotoSession}-current` const SESSION_ID = `${DataType.AtprotoSession}-current`
@@ -8,12 +8,15 @@ export const loadSession = (): Promise<AtprotoSession | null> => {
return data.get<DataType.AtprotoSession, AtprotoSession>(SESSION_ID) 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 = { const session: AtprotoSession = {
_id: SESSION_ID, _id: SESSION_ID,
$type: DataType.AtprotoSession, $type: DataType.AtprotoSession,
did, did,
handle, handle
} }
await data.update<DataType.AtprotoSession, AtprotoSession>(session) 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 let cursor: string | undefined
do { do {
const url = new URL('https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows') const url = new URL(
url.searchParams.set('actor', did) "https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows"
url.searchParams.set('limit', '100') )
url.searchParams.set("actor", did)
url.searchParams.set("limit", "100")
if (cursor) { if (cursor) {
url.searchParams.set('cursor', cursor) url.searchParams.set("cursor", cursor)
} }
const response = await fetch(url) 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) { for (const follow of result.follows) {
follows.add(follow.did) follows.add(follow.did)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import { Ref, toValue } from 'vue' import { Ref, toValue } from "vue"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { History } from '@/data/models/History' 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 const MAX_REPO_HISTORY = 10
export const useVisitRepo = (newRepo: { export const useVisitRepo = (newRepo: {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import { initContract } from "@ts-rest/core" import { initContract } from "@ts-rest/core"
import { type } from "arktype"
import { initQueryClient } from "@ts-rest/vue-query" import { initQueryClient } from "@ts-rest/vue-query"
import { type } from "arktype"
const PublicNoteListItem = type({ const PublicNoteListItem = type({
did: "string", did: "string",
rkey: "string", rkey: "string",
title: "string", title: "string",
publishedAt: "string", publishedAt: "string",
createdAt: "string", createdAt: "string"
}) })
export type PublicNoteListItem = typeof PublicNoteListItem.infer export type PublicNoteListItem = typeof PublicNoteListItem.infer
@@ -18,7 +18,7 @@ const PublicNote = type({
title: "string", title: "string",
content: "string", content: "string",
publishedAt: "string", publishedAt: "string",
createdAt: "string", createdAt: "string"
}) })
export type PublicNote = typeof PublicNote.infer export type PublicNote = typeof PublicNote.infer
@@ -31,34 +31,34 @@ export const noteRouter = contract.router({
path: "/notes", path: "/notes",
query: type({ query: type({
cursor: "string | undefined", cursor: "string | undefined",
limit: "number | undefined", limit: "number | undefined"
}), }),
responses: { responses: {
200: type({ 200: type({
notes: PublicNoteListItem.array(), notes: PublicNoteListItem.array()
}), })
}, },
summary: "List all notes", summary: "List all notes"
}, },
noteListsByDid: { noteListsByDid: {
method: "GET", method: "GET",
path: "/:did/notes", path: "/:did/notes",
pathParams: type({ pathParams: type({
did: "string", did: "string"
}), }),
query: type({ query: type({
cursor: "string | undefined", cursor: "string | undefined",
limit: "number | undefined", limit: "number | undefined"
}), }),
responses: { responses: {
200: type({ 200: type({
notes: PublicNoteListItem.array(), notes: PublicNoteListItem.array()
}), })
},
summary: "List all notes",
}, },
summary: "List all notes"
}
}) })
export const client = initQueryClient(noteRouter, { 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 { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { useRepos } from '@/hooks/useRepos.hook' import { useRepos } from "@/hooks/useRepos.hook"
import { RepoBase } from '@/modules/repo/interfaces/RepoBase' import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
import { FavoriteRepo } from '@/modules/repo/models/FavoriteRepo' import { FavoriteRepo } from "@/modules/repo/models/FavoriteRepo"
export const useFavoriteRepos = () => { export const useFavoriteRepos = () => {
const { repos } = useRepos() const { repos } = useRepos()

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { Model } from '@/data/models/Model' import { Model } from "@/data/models/Model"
import { RepoFile } from '@/modules/repo/interfaces/RepoFile' import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
export interface SavedRepo extends Model<DataType.SavedRepo> { export interface SavedRepo extends Model<DataType.SavedRepo> {
user: string 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> => { export const getOctokit = async (): Promise<Octokit> => {
const response = await getAccessToken() const response = await getAccessToken()
return new Octokit({ 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 ( export const getFiles = async (
owner: string, owner: string,
repo: string, repo: string
): Promise<RepoFile[]> => { ): Promise<RepoFile[]> => {
if (!owner || !repo) { if (!owner || !repo) {
return [] return []
@@ -15,7 +15,7 @@ export const getFiles = async (
const commits = await octokit.request("GET /repos/{owner}/{repo}/commits", { const commits = await octokit.request("GET /repos/{owner}/{repo}/commits", {
owner, owner,
repo, repo
}) })
const lastCommit = commits.data.shift() const lastCommit = commits.data.shift()
@@ -30,8 +30,8 @@ export const getFiles = async (
owner, owner,
repo, repo,
tree_sha: lastCommit.commit.tree.sha, tree_sha: lastCommit.commit.tree.sha,
recursive: "true", recursive: "true"
}, }
) )
return treeResponse?.data.tree.filter((t) => t.type === "blob") ?? [] 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 { render } = markdownBuilder()
const { getCachedNote, saveCacheNote } = prepareNoteCache( const { getCachedNote, saveCacheNote } = prepareNoteCache(
`${owner}-${repo}-README`, `${owner}-${repo}-README`
) )
try { try {
@@ -68,7 +68,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
const README = await octokit.repos.getReadme({ const README = await octokit.repos.getReadme({
owner, owner,
repo, repo
}) })
if (README) { if (README) {
@@ -90,7 +90,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
export const getUserSettingsContent = async ( export const getUserSettingsContent = async (
user: string, user: string,
repo: string, repo: string,
files: RepoFile[], files: RepoFile[]
): Promise<Omit<UserSettings, "chosenFontFamily"> | null> => { ): Promise<Omit<UserSettings, "chosenFontFamily"> | null> => {
const configFile = files.find((file) => file.path === ".remanso.json") const configFile = files.find((file) => file.path === ".remanso.json")
@@ -110,7 +110,7 @@ export const getUserSettingsContent = async (
export const queryFileContent = async ( export const queryFileContent = async (
user: string, user: string,
repo: string, repo: string,
sha: string, sha: string
) => { ) => {
const octokit = await getOctokit() const octokit = await getOctokit()
@@ -123,8 +123,8 @@ export const queryFileContent = async (
{ {
owner: user, owner: user,
repo: repo, repo: repo,
file_sha: sha, file_sha: sha
}, }
) )
return file?.data.content ?? null 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', () => { describe("resolve path service", () => {
it('set the absolute path if path to resolve is empty', () => { it("set the absolute path if path to resolve is empty", () => {
expect(resolvePath('standard/README.md', '')).toEqual('standard/') expect(resolvePath("standard/README.md", "")).toEqual("standard/")
}) })
it('returns the path sanitized if there is no absolute path', () => { it("returns the path sanitized if there is no absolute path", () => {
expect(resolvePath('', './here/note.md')).toEqual('here/note.md') expect(resolvePath("", "./here/note.md")).toEqual("here/note.md")
}) })
it('set the absolute path from the current path', () => { it("set the absolute path from the current path", () => {
expect(resolvePath('standard/README.md', './other-note.md')).toEqual( expect(resolvePath("standard/README.md", "./other-note.md")).toEqual(
'standard/other-note.md' "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( expect(
resolvePath('standard/you/are/here/README.md', './other-note.md') resolvePath("standard/you/are/here/README.md", "./other-note.md")
).toEqual('standard/you/are/here/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( expect(
resolvePath('standard/you/are/here/README.md', '../other-note.md') resolvePath("standard/you/are/here/README.md", "../other-note.md")
).toEqual('standard/you/are/other-note.md') ).toEqual("standard/you/are/other-note.md")
expect( expect(
resolvePath('standard/you/are/here/README.md', '../../other-note.md') resolvePath("standard/you/are/here/README.md", "../../other-note.md")
).toEqual('standard/you/other-note.md') ).toEqual("standard/you/other-note.md")
expect( expect(
resolvePath('standard/you/are/here/README.md', './../../other-note.md') resolvePath("standard/you/are/here/README.md", "./../../other-note.md")
).toEqual('standard/you/other-note.md') ).toEqual("standard/you/other-note.md")
expect( expect(
resolvePath('standard/you/are/here/README.md', './../../../other-note.md') resolvePath("standard/you/are/here/README.md", "./../../../other-note.md")
).toEqual('standard/other-note.md') ).toEqual("standard/other-note.md")
}) })
}) })

View File

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

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