├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .storybook ├── fixtures │ └── recipes.ts ├── main.js ├── preview-head.html ├── preview.ts ├── stories │ ├── AppHeaderCloudStatus.stories.ts │ ├── AppHeaderUserMenu.stories.ts │ ├── AppSnackbar.stories.ts │ ├── RecipeCard.stories.ts │ ├── RecipesGrid.stories.ts │ ├── core │ │ ├── CoreButton.stories.ts │ │ ├── CoreFluidInput.stories.ts │ │ ├── CoreFluidInputList.stories.ts │ │ ├── CoreFluidTextArea.stories.ts │ │ ├── CoreFluidTextAreaList.stories.ts │ │ ├── CoreInput.stories.ts │ │ ├── CoreLink.stories.ts │ │ ├── CoreMarkdown.stories.ts │ │ └── CoreTooltip.stories.ts │ └── modals │ │ └── CloudStatusModal.stories.ts ├── styles.css ├── support │ └── helpers.ts ├── tsconfig.json └── types │ └── storybook.d.ts ├── .vscode └── launch.json ├── 404.html ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cypress.json ├── cypress ├── fixtures │ ├── html │ │ ├── jaimies-hummus.html │ │ ├── juns-ramen.html │ │ ├── pisto.html │ │ └── zurcher-geschnetzeltes.html │ ├── img │ │ └── ramen.png │ ├── recipes │ │ ├── aguachile.json │ │ ├── aguachile.jsonld │ │ ├── aguachile.ttl │ │ ├── dashi-public.ttl │ │ ├── dashi-ramen.ttl │ │ ├── dashi.ttl │ │ ├── jaimies-hummus.json │ │ ├── juns-ramen.json │ │ ├── pisto.json │ │ ├── pisto.ttl │ │ ├── public.ttl │ │ ├── ramen-1.json │ │ ├── ramen-2.json │ │ ├── ramen-3.json │ │ ├── ramen-bare.ttl │ │ ├── ramen-gallery.ttl │ │ ├── ramen-http.ttl │ │ ├── ramen-malformed.ttl │ │ ├── ramen.json │ │ ├── ramen.ttl │ │ └── tombstone.ttl │ ├── sparql │ │ ├── add-private-type-index.sparql │ │ ├── add-public-type-index.sparql │ │ ├── create-ramen.sparql │ │ ├── create-recipes-list.sparql │ │ ├── mend-ramen-bare.sparql │ │ ├── mend-ramen-malformed.sparql │ │ ├── publish-ramen.sparql │ │ ├── reconcile-ramen-history.sparql │ │ ├── reconcile-ramen.sparql │ │ ├── register-cookbook.sparql │ │ ├── remove-type-index.sparql │ │ ├── unpublish-ramen.sparql │ │ ├── update-image.sparql │ │ ├── update-ramen-1.sparql │ │ ├── update-ramen-2.sparql │ │ ├── update-ramen-3.sparql │ │ ├── update-ramen-http.sparql │ │ ├── update-ramen-name.sparql │ │ └── update-ramen-without-history.sparql │ ├── turtle │ │ ├── private-acls.ttl │ │ ├── profile.ttl │ │ ├── public-acls.ttl │ │ ├── public-list-empty.ttl │ │ ├── public-list-with-ramen.ttl │ │ └── public-type-index.ttl │ └── webids │ │ └── alice.ttl ├── integration │ ├── authentication.spec.ts │ ├── authorization.spec.ts │ ├── cookbook.spec.ts │ ├── github.spec.ts │ ├── interoperability.spec.ts │ ├── kitchen.spec.ts │ ├── reactivity.spec.ts │ ├── settings.spec.ts │ └── viewer.spec.ts ├── plugins │ ├── index.ts │ └── tasks.ts ├── screenshots │ └── .gitignore ├── support │ ├── commands.ts │ ├── commands │ │ ├── a11y.ts │ │ ├── app.ts │ │ ├── auth.ts │ │ ├── factory.ts │ │ ├── forms.ts │ │ ├── lifecycle.ts │ │ └── storage.ts │ └── index.ts ├── tsconfig.json └── types │ ├── chai.d.ts │ ├── cypress-fill-command.d.ts │ └── cypress-plugin-tab.d.ts ├── docs ├── contribute-translations.md ├── logo.png ├── secrets.md └── using-a-proxy.md ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── patches └── image-blob-reduce+4.1.0.patch ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── banner.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── favicon.svg ├── mstile-150x150.png ├── robots.txt └── safari-pinned-tab.svg ├── solid-server ├── README.md ├── package-lock.json └── package.json ├── src ├── App.vue ├── assets │ ├── fonts │ │ ├── Livvic-Black-Latin.woff2 │ │ ├── Livvic-Black-LatinExt.woff2 │ │ ├── Livvic-Bold-Latin.woff2 │ │ ├── Livvic-Bold-LatinExt.woff2 │ │ ├── Livvic-ExtraLight-Latin.woff2 │ │ ├── Livvic-ExtraLight-LatinExt.woff2 │ │ ├── Livvic-Italic-Black-Latin.woff2 │ │ ├── Livvic-Italic-Black-LatinExt.woff2 │ │ ├── Livvic-Italic-Bold-Latin.woff2 │ │ ├── Livvic-Italic-Bold-LatinExt.woff2 │ │ ├── Livvic-Italic-ExtraLight-Latin.woff2 │ │ ├── Livvic-Italic-ExtraLight-LatinExt.woff2 │ │ ├── Livvic-Italic-Light-Latin.woff2 │ │ ├── Livvic-Italic-Light-LatinExt.woff2 │ │ ├── Livvic-Italic-Medium-Latin.woff2 │ │ ├── Livvic-Italic-Medium-LatinExt.woff2 │ │ ├── Livvic-Italic-Normal-Latin.woff2 │ │ ├── Livvic-Italic-Normal-LatinExt.woff2 │ │ ├── Livvic-Italic-SemiBold-Latin.woff2 │ │ ├── Livvic-Italic-SemiBold-LatinExt.woff2 │ │ ├── Livvic-Italic-Thin-Latin.woff2 │ │ ├── Livvic-Italic-Thin-LatinExt.woff2 │ │ ├── Livvic-Light-Latin.woff2 │ │ ├── Livvic-Light-LatinExt.woff2 │ │ ├── Livvic-Medium-Latin.woff2 │ │ ├── Livvic-Medium-LatinExt.woff2 │ │ ├── Livvic-Normal-Latin.woff2 │ │ ├── Livvic-Normal-LatinExt.woff2 │ │ ├── Livvic-SemiBold-Latin.woff2 │ │ ├── Livvic-SemiBold-LatinExt.woff2 │ │ ├── Livvic-Thin-Latin.woff2 │ │ ├── Livvic-Thin-LatinExt.woff2 │ │ └── README.md │ ├── icons │ │ ├── scanning.svg │ │ ├── solid-emblem.svg │ │ ├── spinner.svg │ │ └── umai.svg │ ├── sounds │ │ └── timer.mp3 │ └── styles │ │ ├── fonts.css │ │ ├── main.css │ │ └── print.css ├── boot.ts ├── components │ ├── AppFooter.vue │ ├── AppHeader.vue │ ├── AppHeaderButton.vue │ ├── AppHeaderCloudStatus.vue │ ├── AppHeaderUserMenu.vue │ ├── AppKitchen.vue │ ├── AppLayout.vue │ ├── AppOverlays.vue │ ├── AppPage.vue │ ├── AppRouterView.vue │ ├── AppSnackbar.vue │ ├── AppStartupCrash.vue │ ├── base │ │ ├── BaseCheckbox.vue │ │ ├── BaseFileInput.ts │ │ └── BaseFileInput.vue │ ├── cloud │ │ └── CloudConfiguration.vue │ ├── core │ │ ├── CoreButton.vue │ │ ├── CoreClipboard.vue │ │ ├── CoreDetails.vue │ │ ├── CoreFluidActionButton.vue │ │ ├── CoreFluidInput.ts │ │ ├── CoreFluidInput.vue │ │ ├── CoreFluidInputList.vue │ │ ├── CoreFluidInputListItem.vue │ │ ├── CoreFluidTextArea.vue │ │ ├── CoreFluidTextAreaList.vue │ │ ├── CoreFluidTextAreaListItem.vue │ │ ├── CoreForm.vue │ │ ├── CoreFormErrors.vue │ │ ├── CoreInput.vue │ │ ├── CoreInputSubmit.vue │ │ ├── CoreJSON.vue │ │ ├── CoreLink.vue │ │ ├── CoreMarkdown.vue │ │ ├── CoreProseFiller.vue │ │ ├── CoreSearchBox.vue │ │ ├── CoreSelect.vue │ │ ├── CoreSortableList.vue │ │ ├── CoreSortableListItem.vue │ │ ├── CoreTextArea.vue │ │ ├── CoreToggle.vue │ │ ├── CoreTooltip.vue │ │ ├── index.ts │ │ └── lists │ │ │ ├── CoreListItemValue.ts │ │ │ ├── animations.ts │ │ │ └── composables.ts │ ├── modals │ │ ├── AppLoadingModal.vue │ │ ├── AppLoginModal.vue │ │ ├── AppModal.transitions.ts │ │ ├── AppModal.ts │ │ ├── AppModal.vue │ │ ├── AppSnackbars.vue │ │ ├── CloudStatusModal.vue │ │ ├── CloudStatusModalConnected.vue │ │ ├── CloudStatusModalDisconnected.vue │ │ ├── CreateRecipeModal.vue │ │ ├── ErrorReportModal.vue │ │ ├── ErrorReportsModal.vue │ │ ├── SettingsModal.vue │ │ ├── ShareRecipeModal.vue │ │ ├── WebImportHtmlModal.ts │ │ ├── WebImportHtmlModal.vue │ │ ├── WebImportModal.ts │ │ ├── WebImportModal.vue │ │ ├── WebImportModalAdvancedOptions.vue │ │ ├── WebImportModalError.vue │ │ ├── WebImportModalResults.vue │ │ ├── WebImportModalResultsRecipe.vue │ │ ├── WebImportModalResultsRecipeRadio.vue │ │ ├── WebImportModalStart.ts │ │ ├── WebImportModalStart.vue │ │ └── WebImportScanning.vue │ └── recipe │ │ ├── RecipeAccessControl.vue │ │ ├── RecipeCard.transitions.ts │ │ ├── RecipeCard.vue │ │ ├── RecipeCreateOptions.vue │ │ ├── RecipeImage.vue │ │ ├── RecipePage.transitions.ts │ │ ├── RecipePage.ts │ │ ├── RecipePage.vue │ │ ├── RecipePageMetadata.vue │ │ ├── RecipeShareOptions.ts │ │ ├── RecipeShareOptions.vue │ │ ├── RecipeTitle.transitions.ts │ │ ├── RecipeTitle.vue │ │ └── RecipesGrid.vue ├── directives │ └── wobbly-border.ts ├── errors │ └── TooManyRequestsError.ts ├── framework │ ├── auth │ │ ├── Authenticator.mock.ts │ │ ├── Authenticator.ts │ │ ├── authenticators │ │ │ ├── InruptAuthenticator.ts │ │ │ └── LocalStorageAuthenticator.ts │ │ ├── errors │ │ │ └── AuthenticationCancelledError.ts │ │ └── index.ts │ ├── cloud │ │ └── remote_helpers.ts │ ├── components │ │ ├── ConfirmModal.vue │ │ ├── ErrorReport.vue │ │ ├── LoadingModal.vue │ │ ├── MarkdownModal.vue │ │ ├── NotFound.vue │ │ └── headless │ │ │ ├── HeadlessButton.vue │ │ │ ├── HeadlessForm.vue │ │ │ ├── HeadlessInput.d.ts │ │ │ ├── HeadlessInput.vue │ │ │ ├── HeadlessInputError.vue │ │ │ ├── HeadlessInputInput.vue │ │ │ ├── HeadlessInputLabel.vue │ │ │ ├── HeadlessInputTextArea.vue │ │ │ ├── HeadlessLink.vue │ │ │ ├── HeadlessSelect.ts │ │ │ ├── HeadlessSelect.vue │ │ │ └── index.ts │ ├── core │ │ ├── Service.test.ts │ │ ├── Service.ts │ │ ├── errors │ │ │ └── ServiceBootError.ts │ │ ├── facades │ │ │ ├── App.ts │ │ │ ├── Auth.mock.ts │ │ │ ├── Auth.ts │ │ │ ├── AutoLinking.ts │ │ │ ├── Browser.ts │ │ │ ├── Cache.ts │ │ │ ├── Cloud.mock.ts │ │ │ ├── Cloud.ts │ │ │ ├── ElementTransitions.ts │ │ │ ├── Errors.ts │ │ │ ├── Events.ts │ │ │ ├── Files.ts │ │ │ ├── I18n.ts │ │ │ ├── Lang.ts │ │ │ ├── Network.ts │ │ │ ├── Router.ts │ │ │ ├── Store.ts │ │ │ └── UI.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── AppService.ts │ │ │ ├── AuthService.mock.ts │ │ │ ├── AuthService.ts │ │ │ ├── AutoLinkingService.ts │ │ │ ├── BrowserService.ts │ │ │ ├── CacheService.ts │ │ │ ├── CloudService.mock.ts │ │ │ ├── CloudService.test.ts │ │ │ ├── CloudService.ts │ │ │ ├── ElementTransitionsService.ts │ │ │ ├── ErrorsService.ts │ │ │ ├── EventsService.ts │ │ │ ├── FilesService.ts │ │ │ ├── LangService.ts │ │ │ ├── NetworkService.ts │ │ │ └── UIService.ts │ ├── directives │ │ ├── element-transitions.ts │ │ ├── focus-visible.ts │ │ ├── index.ts │ │ ├── initial-focus.ts │ │ └── safe-html.ts │ ├── forms │ │ ├── Form.ts │ │ ├── FormInput.ts │ │ ├── FormValue.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── rules │ │ │ ├── index.ts │ │ │ └── required.ts │ ├── index.ts │ ├── plugins │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── router.ts │ │ └── vuex.ts │ ├── routing │ │ ├── github │ │ │ └── github-pages-404.ts │ │ ├── index.ts │ │ └── router │ │ │ ├── FrameworkRouter.ts │ │ │ └── index.ts │ ├── testing │ │ ├── model-helpers.ts │ │ ├── service-helpers.ts │ │ └── utils.ts │ ├── types │ │ ├── shims-vue.d.ts │ │ └── shims-yaml.d.ts │ ├── utils │ │ ├── composition │ │ │ ├── events.ts │ │ │ ├── list-dragging.ts │ │ │ └── observers.ts │ │ ├── datetime.test.ts │ │ ├── datetime.ts │ │ ├── dom.ts │ │ ├── memoize.ts │ │ ├── sanitization.ts │ │ ├── sentry.lazy.ts │ │ ├── tailwindcss.test.ts │ │ ├── tailwindcss.ts │ │ ├── transitions.ts │ │ ├── translate.ts │ │ ├── vite.ts │ │ └── vue.ts │ ├── vite-plugin-index-replacements │ │ └── index.ts │ └── vite-plugin-solid-clientid │ │ ├── index.ts │ │ └── vite-plugin-solid-clientid.d.ts ├── lang │ ├── ca.yaml │ ├── en.yaml │ ├── es.yaml │ └── locales.json ├── main.testing.ts ├── main.ts ├── models │ ├── Dish.ts │ ├── Recipe.schema.ts │ ├── Recipe.ts │ ├── RecipeInstructionsStep.schema.ts │ ├── RecipeInstructionsStep.ts │ ├── RecipesContainer.ts │ ├── RecipesList.schema.ts │ ├── RecipesList.ts │ ├── RecipesListItem.schema.ts │ ├── RecipesListItem.ts │ ├── Timer.ts │ └── contracts │ │ └── RecipesCollection.ts ├── plugins │ ├── index.ts │ ├── marked.ts │ └── soukai.ts ├── routing │ ├── index.ts │ └── pages │ │ ├── home │ │ ├── Home.vue │ │ └── components │ │ │ ├── HomeCreateCookbook.vue │ │ │ ├── HomeCreateRecipe.vue │ │ │ ├── HomeHeading.vue │ │ │ ├── HomeOnboarding.vue │ │ │ ├── HomeOnboardingCreateRecipe.vue │ │ │ ├── HomeOnboardingLogin.vue │ │ │ ├── HomeRecipes.vue │ │ │ └── HomeRecipesFAB.vue │ │ ├── kitchen │ │ ├── Kitchen.vue │ │ ├── KitchenCompleted.vue │ │ ├── KitchenIngredients.vue │ │ ├── KitchenInstructions.vue │ │ ├── components │ │ │ ├── KitchenPage.transitions.ts │ │ │ ├── KitchenPage.vue │ │ │ ├── KitchenTimer.vue │ │ │ └── modals │ │ │ │ ├── KitchenAddTimerModal.vue │ │ │ │ ├── KitchenTimeoutModal.vue │ │ │ │ └── KitchenTimersModal.vue │ │ └── constants.ts │ │ ├── recipes │ │ ├── RecipesCreate.vue │ │ ├── RecipesEdit.vue │ │ ├── RecipesHistory.vue │ │ ├── RecipesShow.vue │ │ └── components │ │ │ ├── RecipeDetails.transitions.ts │ │ │ ├── RecipeDetails.vue │ │ │ ├── RecipeForm.transitions.ts │ │ │ ├── RecipeForm.vue │ │ │ └── modals │ │ │ ├── RecipeImageFormModal.d.ts │ │ │ └── RecipeImageFormModal.vue │ │ └── viewer │ │ ├── Viewer.vue │ │ └── components │ │ ├── ViewerForm.vue │ │ ├── ViewerRecipe.transitions.ts │ │ ├── ViewerRecipe.vue │ │ ├── ViewerRecipeCreator.vue │ │ ├── ViewerRecipes.transitions.ts │ │ └── ViewerRecipes.vue ├── services │ ├── ConfigService.ts │ ├── CookbookService.test.ts │ ├── CookbookService.ts │ ├── KitchenService.ts │ ├── ViewerService.ts │ ├── cloud-handlers │ │ └── recipes.ts │ ├── errors │ │ └── cookbook-exists-error.ts │ ├── facades │ │ ├── Config.ts │ │ ├── Cookbook.ts │ │ ├── Kitchen.ts │ │ └── Viewer.ts │ └── index.ts ├── testing │ └── setup.ts ├── tests │ └── fixtures │ │ └── recipes │ │ └── keto-bolognese.html ├── types │ ├── env.d.ts │ ├── image-blob-reduce.d.ts │ ├── jest.d.ts │ ├── jsonld-shim.d.ts │ └── testing.d.ts └── utils │ ├── ListenersManager.ts │ ├── ingredients.test.ts │ ├── ingredients.ts │ ├── intervals.ts │ ├── markdown.test.ts │ ├── markdown.ts │ ├── urls.ts │ ├── web-parsing.test.ts │ └── web-parsing.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,ts,vue}] 2 | indent_style = space 3 | indent_size = 4 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_DEFAULT_PROXY_URL= 2 | VITE_SENTRY_DSN= 3 | VITE_APP_DOMAIN= 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.storybook 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@noeldemartin/eslint-config-vue'], 3 | }; 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version-file: '.nvmrc' 13 | - run: npm ci 14 | # - run: npm ci 15 | # working-directory: solid-server 16 | - run: npm run lint 17 | - run: npm run build 18 | - run: npm run test 19 | # TODO fix CI, getting error "OIDC request failed: SSL is required for client_id authentication unless working locally." :/ 20 | # - run: npm run cy:test 21 | # - name: Upload Cypress screenshots 22 | # uses: actions/upload-artifact@v3 23 | # if: ${{ failure() }} 24 | # with: 25 | # name: cypress_screenshots 26 | # path: cypress/screenshots 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.* 3 | !.env.example 4 | cypress/downloads 5 | dist/ 6 | node_modules/ 7 | solid-server/data 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.15.0 2 | -------------------------------------------------------------------------------- /.storybook/fixtures/recipes.ts: -------------------------------------------------------------------------------- 1 | import Recipe from '@/models/Recipe'; 2 | 3 | export enum RecipeFixture { 4 | Ramen = 'ramen', 5 | Pisto = 'pisto', 6 | Unknown = 'unknown', 7 | } 8 | 9 | export const recipeFixtures: Record = { 10 | [RecipeFixture.Ramen]: new Recipe({ 11 | url: `https://pod.example.com/${RecipeFixture.Ramen}`, 12 | name: 'Ramen', 13 | imageUrls: ['https://images.unsplash.com/photo-1557872943-16a5ac26437e?w=500'], 14 | }), 15 | [RecipeFixture.Pisto]: new Recipe({ 16 | url: `https://pod.example.com/${RecipeFixture.Pisto}`, 17 | name: 'Pisto', 18 | imageUrls: ['https://images.unsplash.com/photo-1572453800999-e8d2d1589b7c?w=500'], 19 | }), 20 | [RecipeFixture.Unknown]: new Recipe({ 21 | url: `https://pod.example.com/${RecipeFixture.Unknown}`, 22 | }), 23 | }; 24 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import { app } from '@storybook/vue3'; 2 | import { bootSolidModels } from 'soukai-solid'; 3 | import { createStore } from 'vuex'; 4 | 5 | import i18n from '@/framework/plugins/i18n'; 6 | import Store from '@/framework/core/facades/Store'; 7 | import { mockService } from '@/framework/testing/service-helpers'; 8 | import { services } from '@/framework/core'; 9 | import type { MockServices } from '@/framework/testing/service-helpers'; 10 | import type { Services } from '@/framework/core'; 11 | 12 | import '@/assets/styles/main.css'; 13 | import './styles.css'; 14 | import components from '@/framework/components/headless'; 15 | import directives from '@/framework/directives'; 16 | import router from '@/framework/plugins/router'; 17 | 18 | bootSolidModels(); 19 | Store.setInstance(createStore({ strict: true })); 20 | 21 | for (const [name, facade] of Object.entries(services)) { 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | facade.setInstance(mockService(name.slice(1), facade.instance?.static() as any) as any); 24 | } 25 | 26 | app.use(await i18n()); 27 | app.use(router([ 28 | { name: 'recipes.show', path: '/recipes/:recipe', component: {} }, 29 | ])); 30 | Object.entries(components).forEach(([name, component]) => app.component(name, component)); 31 | Object.entries(directives).forEach(([name, directive]) => app.directive(name, directive)); 32 | Object.assign(app.config.globalProperties, services); 33 | window.Storybook = services as unknown as MockServices; 34 | -------------------------------------------------------------------------------- /.storybook/stories/RecipeCard.stories.ts: -------------------------------------------------------------------------------- 1 | import { meta, story, template } from '@sb/support/helpers'; 2 | import { RecipeFixture, recipeFixtures } from '@sb/fixtures/recipes'; 3 | 4 | import RecipeCard from '@/components/recipe/RecipeCard.vue'; 5 | 6 | interface Args { 7 | recipe: RecipeFixture; 8 | } 9 | 10 | const Template = template(({ recipe }) => { 11 | return { 12 | components: { RecipeCard }, 13 | setup: () => ({ recipe: recipeFixtures[recipe] }), 14 | template: '
', 15 | }; 16 | }); 17 | 18 | export const Showcase = story(template(({ recipe }) => ({ 19 | components: { RecipeCard }, 20 | setup: () => ({ recipe: recipeFixtures[recipe] }), 21 | template: ` 22 |
23 |

Default

24 |
25 |
26 | 27 |
28 |

Hover

29 |
30 |
31 | 32 |
33 |

Keyboard focus

34 |
35 |
36 | `, 37 | }))); 38 | 39 | export const Playground = story(Template); 40 | 41 | export default meta({ 42 | component: RecipeCard, 43 | title: 'WIP/RecipeCard', 44 | argTypes: { 45 | recipe: { 46 | control: { type: 'select' }, 47 | options: RecipeFixture, 48 | }, 49 | }, 50 | args: { 51 | recipe: RecipeFixture.Ramen, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /.storybook/stories/RecipesGrid.stories.ts: -------------------------------------------------------------------------------- 1 | import { meta, story, template } from '@sb/support/helpers'; 2 | import { recipeFixtures } from '@sb/fixtures/recipes'; 3 | 4 | import RecipesGrid from '@/components/recipe/RecipesGrid.vue'; 5 | 6 | interface Args { 7 | // 8 | } 9 | 10 | const Template = template(() => { 11 | return { 12 | components: { RecipesGrid }, 13 | setup: () => ({ recipes: Object.values(recipeFixtures) }), 14 | template: '
', 15 | }; 16 | }); 17 | 18 | export const Base = story(Template); 19 | 20 | export default meta({ 21 | component: RecipesGrid, 22 | title: 'WIP/RecipesGrid', 23 | }); 24 | -------------------------------------------------------------------------------- /.storybook/stories/core/CoreFluidInputList.stories.ts: -------------------------------------------------------------------------------- 1 | import { meta, story } from '@sb/support/helpers'; 2 | 3 | import CoreFluidInputList from '@/components/core/CoreFluidInputList.vue'; 4 | import CoreListItemValue from '@/components/core/lists/CoreListItemValue'; 5 | 6 | const Meta = meta({ 7 | component: CoreFluidInputList, 8 | title: 'Core/Fluid Input List', 9 | }); 10 | 11 | export default Meta; 12 | 13 | export const Playground = story(() => ({ 14 | components: { CoreFluidInputList }, 15 | data: () => ({ 16 | items: [ 17 | new CoreListItemValue('Item #1'), 18 | new CoreListItemValue('Item #2'), 19 | new CoreListItemValue, 20 | ], 21 | }), 22 | mounted() { 23 | this.$refs.$root.focus(); 24 | }, 25 | template: ` 26 | 32 | `, 33 | })); 34 | -------------------------------------------------------------------------------- /.storybook/stories/core/CoreFluidTextAreaList.stories.ts: -------------------------------------------------------------------------------- 1 | import { meta, story } from '@sb/support/helpers'; 2 | 3 | import CoreFluidTextAreaList from '@/components/core/CoreFluidTextAreaList.vue'; 4 | import CoreListItemValue from '@/components/core/lists/CoreListItemValue'; 5 | 6 | const Meta = meta({ 7 | component: CoreFluidTextAreaList, 8 | title: 'Core/Fluid TextArea List', 9 | }); 10 | 11 | export default Meta; 12 | 13 | export const Playground = story(() => ({ 14 | components: { CoreFluidTextAreaList }, 15 | data: () => ({ 16 | items: [ 17 | new CoreListItemValue([ 18 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 19 | 'Donec nulla nisl, commodo fermentum auctor ac, rhoncus non dui.', 20 | 'Suspendisse quam lorem, tincidunt a sem eu, porttitor facilisis libero. ' + 21 | 'Nulla nec mauris neque.', 22 | 'Donec a mauris in tortor euismod facilisis. Aenean porta vel dolor eu porttitor', 23 | ].join('\n\n')), 24 | new CoreListItemValue, 25 | ], 26 | }), 27 | mounted() { 28 | this.$refs.$root.focus(); 29 | }, 30 | template: ` 31 |
32 |
33 | 40 |
41 |
42 | `, 43 | })); 44 | -------------------------------------------------------------------------------- /.storybook/stories/core/CoreMarkdown.stories.ts: -------------------------------------------------------------------------------- 1 | import { meta, story } from '@sb/support/helpers'; 2 | 3 | import CoreMarkdown from '@/components/core/CoreMarkdown.vue'; 4 | 5 | interface Args { 6 | text: string; 7 | } 8 | 9 | const Meta = meta({ 10 | component: CoreMarkdown, 11 | title: 'Core/Markdown', 12 | args: { 13 | text: ` 14 | ## Markdown content 15 | 16 | As you would expect, it supports **bold**, *italic*, and [links](/). 17 | 18 | Also lists: 19 | - one 20 | - two 21 | - three 22 | 23 | And even custom links. 24 | ` 25 | .split('\n') 26 | .map(line => line.trim()) 27 | .join('\n') 28 | .trim(), 29 | }, 30 | argTypes: { 31 | text: { type: 'string' }, 32 | }, 33 | }); 34 | 35 | export default Meta; 36 | 37 | export const Playground = story((args) => ({ 38 | components: { CoreMarkdown }, 39 | data: () => args, 40 | template: '', 41 | })); 42 | -------------------------------------------------------------------------------- /.storybook/stories/core/CoreTooltip.stories.ts: -------------------------------------------------------------------------------- 1 | import { meta, story } from '@sb/support/helpers'; 2 | 3 | import CoreButton from '@/components/core/CoreButton.vue'; 4 | import CoreTooltip from '@/components/core/CoreTooltip.vue'; 5 | 6 | interface Args { 7 | text: string; 8 | } 9 | 10 | const Meta = meta({ 11 | component: CoreTooltip, 12 | title: 'Core/Tooltip', 13 | args: { 14 | text: 'Something interesting about this button', 15 | }, 16 | argTypes: { 17 | text: { type: 'string' }, 18 | }, 19 | }); 20 | 21 | export default Meta; 22 | 23 | export const Playground = story((args) => ({ 24 | components: { 25 | CoreTooltip, 26 | CoreButton, 27 | }, 28 | data: () => args, 29 | template: ` 30 | 31 | 36 | Hover me! 37 | 38 | 39 | `, 40 | })); 41 | -------------------------------------------------------------------------------- /.storybook/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | 5 | #root { 6 | @apply flex flex-col justify-center items-center h-full space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0; 7 | } 8 | -------------------------------------------------------------------------------- /.storybook/support/helpers.ts: -------------------------------------------------------------------------------- 1 | import { tap } from '@noeldemartin/utils'; 2 | import type { Meta, Story } from '@storybook/vue3/types-6-0'; 3 | 4 | export function meta(meta: Meta): Meta { 5 | return meta; 6 | } 7 | 8 | export function story(template: Story, args: Partial = {}): Story { 9 | return tap(template.bind({}), story => { 10 | story.args = args; 11 | }); 12 | } 13 | 14 | export function template(story: Story): Story { 15 | return story; 16 | } 17 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["../src/*"], 7 | "@sb/*": ["./*"] 8 | } 9 | }, 10 | "include": [ 11 | "./**/*.ts", 12 | "../src/**/*.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.storybook/types/storybook.d.ts: -------------------------------------------------------------------------------- 1 | import type { MockServices } from '@/framework/testing/service-helpers'; 2 | import type { Services } from '@/framework/core'; 3 | 4 | declare global { 5 | 6 | const Storybook: MockServices; 7 | 8 | interface Window { 9 | Storybook: MockServices; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "runtimeVersion": "16.15.0", 7 | "request": "launch", 8 | "name": "Jest All", 9 | "program": "${workspaceFolder}/node_modules/.bin/jest", 10 | "args": ["--runInBand"], 11 | "console": "integratedTerminal", 12 | "internalConsoleOptions": "neverOpen", 13 | "disableOptimisticBPs": true, 14 | "windows": { 15 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 16 | } 17 | }, 18 | { 19 | "type": "node", 20 | "runtimeVersion": "16.15.0", 21 | "request": "launch", 22 | "name": "Jest Current File", 23 | "program": "${workspaceFolder}/node_modules/.bin/jest", 24 | "args": [ 25 | "${fileBasenameNoExtension}", 26 | "--config", 27 | "jest.config.js" 28 | ], 29 | "console": "integratedTerminal", 30 | "internalConsoleOptions": "neverOpen", 31 | "disableOptimisticBPs": true, 32 | "windows": { 33 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Umai 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | I am open to contributions, but if you're thinking on some big feature or refactor, you should open an issue to discuss it first. That will increase the chances of it getting merged, and it will prevent you from wasting your time if it's something I'm not willing to integrate. You can also open a draft PR if you want to support your idea with some code, even if it's not finished yet. 4 | 5 | If you're just proposing a small change or fix, you probably don't need to discuss anything, just open a PR :). 6 | 7 | If you want to contribute translations, read the [translations contribution guide](./docs/contribute-translations.md). 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Umai ![CI](https://github.com/NoelDeMartin/umai/actions/workflows/ci.yml/badge.svg) 2 | 3 |

4 | Umai logo 5 |

6 | 7 | Umai is a simple Recipes Manager to collect all your precious recipes. 8 | 9 | - 📗 Create your own cookbook. 10 | - ✨ Import recipes from any website. 11 | - 🧙 Adjust ingredient quantities automatically. 12 | - 👨‍🍳 Share recipes with your friends. 13 | - 📱 Use it completely offline (it's an offline first PWA!). 14 | - 🔐 Keep data safe in your [Solid POD](https://solidproject.org/). 15 | 16 | ## Under the hood 17 | 18 | If you're curious to learn how this came to be, I've been keeping a development journal: [ 19 | Implementing a Recipes Manager using Solid](https://noeldemartin.com/tasks/implementing-a-recipes-manager-using-solid). 20 | 21 | There is currently no documentation for developers, but you can get started by learning its main technologies: 22 | 23 | - [Vue](https://vuejs.org) 24 | - [Tailwind CSS](https://tailwindcss.com) 25 | - [Soukai Solid](https://github.com/NoelDeMartin/soukai-solid) 26 | 27 | ## Attributions 28 | 29 | - [Pepicons](https://pepicons.com/) by [CyCraft](https://cycraft.co/). 30 | - [Zondicons](https://www.zondicons.com/) by [Steve Schoger](https://twitter.com/steveschoger). 31 | - [Livvic](https://github.com/Fonthausen/Livvic) font by [Jacques Le Bailly](https://github.com/Fonthausen). 32 | - Timer sound alarm by [DuckDuckGo](https://github.com/duckduckgo/zeroclickinfo-goodies/blob/master/share/goodie/timer/alarm.mp3). 33 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:5001", 3 | "video": false, 4 | "chromeWebSecurity": false, 5 | "retries": { 6 | "runMode": 5, 7 | "openMode": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cypress/fixtures/img/ramen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/cypress/fixtures/img/ramen.png -------------------------------------------------------------------------------- /cypress/fixtures/recipes/dashi-public.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix purl: . 3 | 4 | <#it> 5 | a schema:ItemList ; 6 | schema:name "Ingredients for Dashi" ; 7 | schema:itemListElement <#ramen>, <#dashi> . 8 | 9 | <#ramen> 10 | a schema:ListItem ; 11 | schema:item . 12 | 13 | <#dashi> 14 | a schema:ListItem ; 15 | schema:item . 16 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/dashi.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix crdt: . 3 | @prefix purl: . 4 | @prefix xsd: . 5 | 6 | <#it> 7 | a schema:Recipe; 8 | schema:name "Dashi"; 9 | schema:description "A basic ingredient for [Ramen](https://pod.example.com/recipes/ramen#it)."; 10 | schema:sameAs "https://recipes.example.com/dashi" ; 11 | purl:isReferencedBy . 12 | 13 | <#it-metadata> 14 | a crdt:Metadata ; 15 | crdt:resource <#it> ; 16 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 17 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 18 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/juns-ramen.json: -------------------------------------------------------------------------------- 1 | {"@graph":[{"@context":{"@vocab":"https://schema.org/"},"@type":"Recipe","name":"Jun's Ramen","description":"=^・ェ・^= Cat Merch! https://crowdmade.com/collections/junskitchen- ►Patreon: https://www.patreon.com/JunsKitchen►EQUIPMENT I use in my channel that you can b...","image":{"@id":"https://i.ytimg.com/vi/9WXIrnWsaCo/hqdefault.jpg"},"sameAs":{"@id":"https://recipes.example.com/ramen"},"@id":"solid://recipes/juns-ramen#it"},{"@context":{"@vocab":"https://vocab.noeldemartin.com/crdt/"},"@type":"Metadata","resource":{"@id":"solid://recipes/juns-ramen#it"},"createdAt":{"@type":"http://www.w3.org/2001/XMLSchema#dateTime","@value":"[[.*]]"},"updatedAt":{"@type":"http://www.w3.org/2001/XMLSchema#dateTime","@value":"[[.*]]"},"@id":"solid://recipes/juns-ramen#it-metadata"}]} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/pisto.json: -------------------------------------------------------------------------------- 1 | {"@graph":[{"@context":{"@vocab":"https://schema.org/"},"@type":"Recipe","name":"Pisto Manchego","cookTime":"PT1H","description":"Pisto Manchego With Olive Oil, Onion, Garlic, Zucchini, Red Bell Pepper, Green Bell Pepper, Tomatoes, Salt, Pepper, Parsley","image":{"@id":"https://lh3.googleusercontent.com/uJ7hLUjesMnUHzeaGqP2doFTYgXDj2pN-ZeDs-1Lyqn_ITHi8BoqNWvUT9EL-x3UHe1s50hEIumrDCHjjEl9jQ=w1280-h1280-c-rj-v1-e365"},"recipeYield":"8","recipeIngredient":["4 zucchini small, cubed","2 cloves garlic finely chopped","2 onion medium or 1 large, chopped","2 tablespoons parsley chopped","2 tomatoes large beef, or 4 medium tomatoes, skinned and cubed","1 green bell pepper","1 red bell pepper","1/2 cup olive oil","pepper","salt"],"sameAs":{"@id":"https://recipes.example.com/pisto"},"@id":"solid://recipes/pisto-manchego#it"},{"@context":{"@vocab":"https://vocab.noeldemartin.com/crdt/"},"@type":"Metadata","resource":{"@id":"solid://recipes/pisto-manchego#it"},"createdAt":{"@type":"http://www.w3.org/2001/XMLSchema#dateTime","@value":"[[.*]]"},"updatedAt":{"@type":"http://www.w3.org/2001/XMLSchema#dateTime","@value":"[[.*]]"},"@id":"solid://recipes/pisto-manchego#it-metadata"}]} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/pisto.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix crdt: . 3 | @prefix purl: . 4 | @prefix xsd: . 5 | 6 | <#it> 7 | a schema:Recipe ; 8 | schema:name "Pisto" ; 9 | schema:description "Pisto is the same as Ratatouille!"; 10 | schema:recipeIngredient "Eggplant", "Zucchini", "Onion", "Tomatoes" ; 11 | purl:isReferencedBy . 12 | 13 | <#it-metadata> 14 | a crdt:Metadata ; 15 | crdt:resource <#it> ; 16 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 17 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 18 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/public.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix purl: . 3 | 4 | <#it> 5 | a schema:ItemList ; 6 | schema:name "Alice's Public Recipes" ; 7 | purl:creator ; 8 | schema:itemListElement <#ramen>, <#pisto> . 9 | 10 | <#ramen> 11 | a schema:ListItem ; 12 | schema:item . 13 | 14 | <#pisto> 15 | a schema:ListItem ; 16 | schema:item . 17 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/ramen-bare.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | 3 | <#it> 4 | a schema:Recipe; 5 | schema:name "Ramen". 6 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/ramen-gallery.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | 3 | <#it> 4 | a schema:Recipe; 5 | schema:name "Ramen"; 6 | schema:image 7 | , 8 | . 9 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/ramen-http.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix crdt: . 3 | @prefix purl: . 4 | @prefix xsd: . 5 | 6 | <#it> 7 | a schema:Recipe; 8 | schema:name "Ramen"; 9 | schema:recipeIngredient "Broth", "Noodles"; 10 | schema:recipeInstructions <#instruction-step-1>, <#instruction-step-2>. 11 | 12 | <#it-metadata> 13 | a crdt:Metadata ; 14 | crdt:resource <#it> ; 15 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 16 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 17 | 18 | <#instruction-step-1> 19 | a schema:HowToStep ; 20 | schema:text "Step 1" ; 21 | schema:position 1 . 22 | 23 | <#instruction-step-1-metadata> 24 | a crdt:Metadata ; 25 | crdt:resource <#instruction-step-1> ; 26 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 27 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 28 | 29 | <#instruction-step-2> 30 | a schema:HowToStep ; 31 | schema:text "Step 2" ; 32 | schema:position 2 . 33 | 34 | <#instruction-step-2-metadata> 35 | a crdt:Metadata ; 36 | crdt:resource <#instruction-step-2> ; 37 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 38 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 39 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/ramen-malformed.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | 3 | <#it> 4 | a schema:Recipe; 5 | schema:name "Ramen"; 6 | schema:image "https://example.com/ramen.png"; 7 | schema:recipeInstructions "Step 1", "Step 2", "Step 3"; 8 | schema:sameAs "https://1.example.com/recipes/ramen", "https://2.example.com/recipes/ramen". 9 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/ramen.json: -------------------------------------------------------------------------------- 1 | {"@graph":[{"@context":{"@vocab":"https://schema.org/"},"@type":"Recipe","name":"Ramen","cookTime":"PT1H30M","prepTime":"P1D","recipeYield":"3 persons","@id":"solid://recipes/ramen#it"},{"@context":{"@vocab":"https://vocab.noeldemartin.com/crdt/"},"@type":"Metadata","resource":{"@id":"solid://recipes/ramen#it"},"createdAt":{"@type":"http://www.w3.org/2001/XMLSchema#dateTime","@value":"[[.*]]"},"updatedAt":{"@type":"http://www.w3.org/2001/XMLSchema#dateTime","@value":"[[.*]]"},"@id":"solid://recipes/ramen#it-metadata"}]} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/ramen.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix crdt: . 3 | @prefix purl: . 4 | @prefix xsd: . 5 | 6 | <#it> 7 | a schema:Recipe; 8 | schema:name "Ramen"; 9 | schema:recipeIngredient "Broth", "Noodles"; 10 | schema:recipeInstructions <#instruction-step-1>, <#instruction-step-2>; 11 | purl:isReferencedBy . 12 | 13 | <#it-metadata> 14 | a crdt:Metadata ; 15 | crdt:resource <#it> ; 16 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 17 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 18 | 19 | <#instruction-step-1> 20 | a schema:HowToStep ; 21 | schema:text "Step 1" ; 22 | schema:position 1 . 23 | 24 | <#instruction-step-1-metadata> 25 | a crdt:Metadata ; 26 | crdt:resource <#instruction-step-1> ; 27 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 28 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 29 | 30 | <#instruction-step-2> 31 | a schema:HowToStep ; 32 | schema:text "Step 2" ; 33 | schema:position 2 . 34 | 35 | <#instruction-step-2-metadata> 36 | a crdt:Metadata ; 37 | crdt:resource <#instruction-step-2> ; 38 | crdt:createdAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime ; 39 | crdt:updatedAt "2021-07-24T00:00:00.000Z"^^xsd:dateTime . 40 | -------------------------------------------------------------------------------- /cypress/fixtures/recipes/tombstone.ttl: -------------------------------------------------------------------------------- 1 | @prefix crdt: . 2 | @prefix xsd: . 3 | 4 | <#it-metadata> 5 | a crdt:Tombstone ; 6 | crdt:resource <#it> ; 7 | crdt:deletedAt "2022-06-12T00:00:00.000Z"^^xsd:dateTime . 8 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/add-private-type-index.sparql: -------------------------------------------------------------------------------- 1 | INSERT DATA { 2 | 3 | 4 | . 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/add-public-type-index.sparql: -------------------------------------------------------------------------------- 1 | INSERT DATA { 2 | 3 | 4 | . 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/create-ramen.sparql: -------------------------------------------------------------------------------- 1 | INSERT DATA { 2 | @prefix schema: . 3 | @prefix crdt: . 4 | @prefix dcmi: . 5 | @prefix xsd: . 6 | 7 | <#it> 8 | a schema:Recipe ; 9 | schema:name "Ramen" ; 10 | schema:recipeIngredient "Broth", "Noodles" ; 11 | schema:recipeInstructions <#[[instruction-step-1][%uuid%]]>, <#[[instruction-step-2][%uuid%]]> . 12 | 13 | <#it-metadata> 14 | a crdt:Metadata ; 15 | crdt:resource <#it> ; 16 | crdt:createdAt "[[date][.*]]"^^xsd:dateTime ; 17 | crdt:updatedAt "[[date][.*]]"^^xsd:dateTime . 18 | 19 | <#[[instruction-step-1][%uuid%]]> 20 | a schema:HowToStep ; 21 | schema:text "Boil the noodles" ; 22 | schema:position 1 . 23 | 24 | <#[[instruction-step-1][%uuid%]]-metadata> 25 | a crdt:Metadata ; 26 | crdt:resource <#[[instruction-step-1][%uuid%]]> ; 27 | crdt:createdAt "[[date][.*]]"^^xsd:dateTime ; 28 | crdt:updatedAt "[[date][.*]]"^^xsd:dateTime . 29 | 30 | <#[[instruction-step-2][%uuid%]]> 31 | a schema:HowToStep ; 32 | schema:text "Dip them into the broth" ; 33 | schema:position 2 . 34 | 35 | <#[[instruction-step-2][%uuid%]]-metadata> 36 | a crdt:Metadata ; 37 | crdt:resource <#[[instruction-step-2][%uuid%]]> ; 38 | crdt:createdAt "[[date][.*]]"^^xsd:dateTime ; 39 | crdt:updatedAt "[[date][.*]]"^^xsd:dateTime . 40 | } 41 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/create-recipes-list.sparql: -------------------------------------------------------------------------------- 1 | INSERT DATA { 2 | @prefix schema: . 3 | @prefix purl: . 4 | 5 | <#it> 6 | a schema:ItemList ; 7 | schema:name "Public Recipes" ; 8 | purl:creator ; 9 | schema:itemListElement <#[[item][%uuid%]]> . 10 | 11 | <#[[item][%uuid%]]> 12 | a schema:ListItem ; 13 | schema:item . 14 | } 15 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/mend-ramen-bare.sparql: -------------------------------------------------------------------------------- 1 | INSERT DATA { 2 | @prefix crdt: . 3 | @prefix xsd: . 4 | 5 | <#it-metadata> 6 | a crdt:Metadata ; 7 | crdt:resource <#it> ; 8 | crdt:createdAt "[[date][.*]]"^^xsd:dateTime ; 9 | crdt:updatedAt "[[date][.*]]"^^xsd:dateTime . 10 | } . 11 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/publish-ramen.sparql: -------------------------------------------------------------------------------- 1 | DELETE DATA { 2 | @prefix crdt: . 3 | @prefix xsd: . 4 | 5 | <#it-metadata> crdt:updatedAt "[[createdAt][.*]]"^^xsd:dateTime . 6 | } ; 7 | INSERT DATA { 8 | @prefix schema: . 9 | @prefix purl: . 10 | @prefix crdt: . 11 | @prefix xsd: . 12 | 13 | <#it> purl:isReferencedBy . 14 | <#it-metadata> crdt:updatedAt "[[updatedAt][.*]]"^^xsd:dateTime . 15 | 16 | <#it-operation-[[name][%uuid%]]> 17 | a crdt:SetPropertyOperation ; 18 | crdt:resource <#it> ; 19 | crdt:date "[[createdAt][.*]]"^^xsd:dateTime ; 20 | crdt:property schema:name ; 21 | crdt:value "Ramen" . 22 | 23 | <#it-operation-[[image][%uuid%]]> 24 | a crdt:SetPropertyOperation ; 25 | crdt:resource <#it> ; 26 | crdt:date "[[createdAt][.*]]"^^xsd:dateTime ; 27 | crdt:property schema:image ; 28 | crdt:value . 29 | 30 | <#it-operation-[[listing][%uuid%]]> 31 | a crdt:AddPropertyOperation ; 32 | crdt:resource <#it> ; 33 | crdt:date "[[updatedAt][.*]]"^^xsd:dateTime ; 34 | crdt:property purl:isReferencedBy ; 35 | crdt:value . 36 | } 37 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/reconcile-ramen.sparql: -------------------------------------------------------------------------------- 1 | DELETE DATA { 2 | @prefix schema: . 3 | @prefix crdt: . 4 | @prefix xsd: . 5 | 6 | <#it> schema:description "is good" . 7 | <#it-metadata> crdt:updatedAt "{{updatedAt}}"^^xsd:dateTime . 8 | } 9 | 10 | ; INSERT DATA { 11 | @prefix schema: . 12 | @prefix crdt: . 13 | @prefix xsd: . 14 | 15 | <#it> schema:description "is good!" . 16 | <#it-metadata> crdt:updatedAt "[[updatedAt][.*]]"^^xsd:dateTime . 17 | 18 | <#it-operation-[[operation][.*]]> 19 | a crdt:SetPropertyOperation ; 20 | crdt:date "[[updatedAt][.*]]"^^xsd:dateTime ; 21 | crdt:property schema:description ; 22 | crdt:resource <#it> ; 23 | crdt:value "is good!" . 24 | } 25 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/register-cookbook.sparql: -------------------------------------------------------------------------------- 1 | INSERT DATA { 2 | <#{{resourceHash}}> 3 | a ; 4 | ; 5 | <{{cookbookUrl}}> . 6 | } 7 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/remove-type-index.sparql: -------------------------------------------------------------------------------- 1 | DELETE DATA { 2 | 3 | 4 | . 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/unpublish-ramen.sparql: -------------------------------------------------------------------------------- 1 | DELETE DATA { 2 | @prefix schema: . 3 | @prefix purl: . 4 | @prefix crdt: . 5 | @prefix xsd: . 6 | 7 | <#it> purl:isReferencedBy . 8 | <#it-metadata> crdt:updatedAt "[[.*]]"^^xsd:dateTime . 9 | } ; 10 | INSERT DATA { 11 | @prefix schema: . 12 | @prefix purl: . 13 | @prefix crdt: . 14 | @prefix xsd: . 15 | 16 | <#it-metadata> crdt:updatedAt "[[updatedAt][.*]]"^^xsd:dateTime . 17 | 18 | <#it-operation-[[listing][%uuid%]]> 19 | a crdt:RemovePropertyOperation ; 20 | crdt:resource <#it> ; 21 | crdt:date "[[updatedAt][.*]]"^^xsd:dateTime ; 22 | crdt:property purl:isReferencedBy ; 23 | crdt:value . 24 | } 25 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/update-ramen-http.sparql: -------------------------------------------------------------------------------- 1 | DELETE DATA { 2 | @prefix schema: . 3 | @prefix crdt: . 4 | @prefix xsd: . 5 | 6 | <#it-metadata> crdt:updatedAt "[[createdAt][.*]]"^^xsd:dateTime . 7 | } ; 8 | 9 | INSERT DATA { 10 | @prefix schema: . 11 | @prefix crdt: . 12 | @prefix xsd: . 13 | 14 | <#it> schema:description "is life" . 15 | 16 | <#it-metadata> crdt:updatedAt "[[updatedAt][.*]]"^^xsd:dateTime . 17 | 18 | <#it-operation-[[operation-1][.*]]> 19 | a crdt:SetPropertyOperation ; 20 | crdt:resource <#it> ; 21 | crdt:date "[[createdAt][.*]]"^^xsd:dateTime ; 22 | crdt:property schema:name ; 23 | crdt:value "Ramen" . 24 | 25 | <#it-operation-[[operation-2][.*]]> 26 | a crdt:SetPropertyOperation ; 27 | crdt:resource <#it> ; 28 | crdt:date "[[createdAt][.*]]"^^xsd:dateTime ; 29 | crdt:property schema:recipeInstructions ; 30 | crdt:value <#instruction-step-1>, <#instruction-step-2> . 31 | 32 | <#it-operation-[[operation-3][.*]]> 33 | a crdt:SetPropertyOperation ; 34 | crdt:resource <#it> ; 35 | crdt:date "[[createdAt][.*]]"^^xsd:dateTime ; 36 | crdt:property schema:recipeIngredient ; 37 | crdt:value "Broth", "Noodles" . 38 | 39 | <#it-operation-[[operation-4][.*]]> 40 | a crdt:SetPropertyOperation ; 41 | crdt:resource <#it> ; 42 | crdt:date "[[updatedAt][.*]]"^^xsd:dateTime ; 43 | crdt:property schema:description ; 44 | crdt:value "is life" . 45 | } 46 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/update-ramen-name.sparql: -------------------------------------------------------------------------------- 1 | DELETE DATA { 2 | @prefix schema: . 3 | @prefix crdt: . 4 | @prefix xsd: . 5 | 6 | <#it> schema:name "Ramen" . 7 | <#it-metadata> crdt:updatedAt "{{createdAt}}"^^xsd:dateTime . 8 | } ; 9 | 10 | INSERT DATA { 11 | @prefix schema: . 12 | @prefix crdt: . 13 | @prefix xsd: . 14 | 15 | <#it> schema:name "Ramen!" . 16 | 17 | <#it-metadata> crdt:updatedAt "{{updatedAt}}"^^xsd:dateTime . 18 | 19 | <#it-operation-00000-1> 20 | a crdt:SetPropertyOperation ; 21 | crdt:date "{{createdAt}}"^^xsd:dateTime ; 22 | crdt:property schema:name ; 23 | crdt:resource <#it> ; 24 | crdt:value "Ramen" . 25 | 26 | <#it-operation-00000-2> 27 | a crdt:SetPropertyOperation ; 28 | crdt:date "{{createdAt}}"^^xsd:dateTime ; 29 | crdt:property schema:description ; 30 | crdt:resource <#it> ; 31 | crdt:value "is good" . 32 | 33 | <#it-operation-00000-3> 34 | a crdt:SetPropertyOperation ; 35 | crdt:date "{{updatedAt}}"^^xsd:dateTime ; 36 | crdt:property schema:name ; 37 | crdt:resource <#it> ; 38 | crdt:value "Ramen!" . 39 | } 40 | -------------------------------------------------------------------------------- /cypress/fixtures/sparql/update-ramen-without-history.sparql: -------------------------------------------------------------------------------- 1 | DELETE DATA { 2 | @prefix schema: . 3 | 4 | <#it> 5 | schema:name "Ramen" ; 6 | schema:recipeIngredient "Broth" . 7 | 8 | <#instruction-step-2> schema:text "Step 2" . 9 | } ; 10 | 11 | INSERT DATA { 12 | @prefix schema: . 13 | 14 | <#it> 15 | schema:name "Ramen!" ; 16 | schema:recipeIngredient "Shiitake" . 17 | 18 | <#instruction-step-2> schema:text "Step two" . 19 | } 20 | -------------------------------------------------------------------------------- /cypress/fixtures/turtle/private-acls.ttl: -------------------------------------------------------------------------------- 1 | @prefix acl: . 2 | 3 | <#owner> 4 | a acl:Authorization ; 5 | acl:agent , ; 6 | acl:accessTo ; 7 | acl:mode acl:Read, acl:Write, acl:Control . 8 | -------------------------------------------------------------------------------- /cypress/fixtures/turtle/profile.ttl: -------------------------------------------------------------------------------- 1 | @prefix foaf: . 2 | @prefix solid: . 3 | @prefix pim: . 4 | 5 | <> a foaf:PersonalProfileDocument . 6 | 7 | <#me> 8 | a foaf:Person; 9 | pim:storage ; 10 | solid:privateTypeIndex . 11 | -------------------------------------------------------------------------------- /cypress/fixtures/turtle/public-acls.ttl: -------------------------------------------------------------------------------- 1 | @prefix acl: . 2 | @prefix foaf: . 3 | 4 | <#public> 5 | a acl:Authorization ; 6 | acl:agentClass foaf:Agent ; 7 | acl:accessTo ; 8 | acl:mode acl:Read . 9 | 10 | <#owner> 11 | a acl:Authorization ; 12 | acl:agent , ; 13 | acl:accessTo ; 14 | acl:mode acl:Read, acl:Write, acl:Control . 15 | -------------------------------------------------------------------------------- /cypress/fixtures/turtle/public-list-empty.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix purl: . 3 | 4 | <#it> 5 | a schema:ItemList ; 6 | schema:name "Public Recipes" ; 7 | purl:creator . 8 | -------------------------------------------------------------------------------- /cypress/fixtures/turtle/public-list-with-ramen.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | @prefix purl: . 3 | 4 | <#it> 5 | a schema:ItemList ; 6 | schema:name "Public Recipes" ; 7 | purl:creator ; 8 | schema:itemListElement <#ramen> . 9 | 10 | <#ramen> 11 | a schema:ListItem ; 12 | schema:item . 13 | -------------------------------------------------------------------------------- /cypress/fixtures/turtle/public-type-index.ttl: -------------------------------------------------------------------------------- 1 | <> a . 2 | 3 | <#public-list> 4 | a ; 5 | ; 6 | . 7 | -------------------------------------------------------------------------------- /cypress/fixtures/webids/alice.ttl: -------------------------------------------------------------------------------- 1 | @prefix foaf: . 2 | @prefix solid: . 3 | @prefix pim: . 4 | 5 | <> a foaf:PersonalProfileDocument . 6 | 7 | <#me> 8 | a foaf:Person ; 9 | foaf:name "Alice" . 10 | -------------------------------------------------------------------------------- /cypress/integration/settings.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Settings', () => { 2 | 3 | beforeEach(() => { 4 | cy.task('resetSolidPOD'); 5 | cy.visit('/recipes?authenticator=localStorage'); 6 | cy.startApp(); 7 | }); 8 | 9 | it('Changes language', () => { 10 | // Arrange 11 | cy.createRecipe({ name: 'Ramen' }); 12 | 13 | // Act 14 | cy.ariaLabel('Open user menu').click(); 15 | cy.press('Settings'); 16 | cy.ariaSelect('Language').select('Català'); 17 | cy.ariaLabel('Close the modal').click(); 18 | 19 | // Assert 20 | cy.see('Nova recepta'); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import tasks from './tasks'; 2 | 3 | module.exports = (on: Cypress.PluginEvents) => on('task', tasks); 4 | -------------------------------------------------------------------------------- /cypress/screenshots/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | import a11yCommands from './commands/a11y'; 2 | import appCommands from './commands/app'; 3 | import authCommands from './commands/auth'; 4 | import factoryCommands from './commands/factory'; 5 | import formsCommands from './commands/forms'; 6 | import lifecycleCommands from './commands/lifecycle'; 7 | import storageCommands from './commands/storage'; 8 | 9 | const commands = { 10 | ...a11yCommands, 11 | ...appCommands, 12 | ...authCommands, 13 | ...factoryCommands, 14 | ...formsCommands, 15 | ...lifecycleCommands, 16 | ...storageCommands, 17 | }; 18 | 19 | type CustomCommands = typeof commands; 20 | 21 | declare global { 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-namespace 24 | namespace Cypress { 25 | interface Chainable extends CustomCommands {} 26 | } 27 | 28 | } 29 | 30 | export default function installCustomCommands(): void { 31 | beforeEach(() => { 32 | cy.resetStorage(); 33 | }); 34 | 35 | for (const [name, implementation] of Object.entries(commands)) { 36 | Cypress.Commands.add( 37 | name as unknown as keyof Cypress.Chainable, 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | implementation as Cypress.CommandFn, 40 | ); 41 | } 42 | 43 | Cypress.Commands.overwrite('reload', (originalReload, ...args) => { 44 | originalReload(...args); 45 | cy.startApp(); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /cypress/support/commands/app.ts: -------------------------------------------------------------------------------- 1 | import type { Services } from '@/framework/core'; 2 | 3 | export default { 4 | 5 | comeBackOnline(): void { 6 | cy.service('$network').then(network => network.online = true); 7 | cy.sync(); 8 | }, 9 | 10 | goOffline(): void { 11 | cy.service('$network').then(network => network.online = false); 12 | }, 13 | 14 | service(name: T): Cypress.Chainable { 15 | return cy.testingRuntime().then(runtime => runtime.getService(name)); 16 | }, 17 | 18 | sync(): void { 19 | cy.press('online'); 20 | cy.press('Synchronize now'); 21 | cy.waitForSync(); 22 | }, 23 | 24 | waitForSync(): void { 25 | cy.see('Syncing in progress'); 26 | cy.see('Syncing is up to date'); 27 | }, 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /cypress/support/commands/factory.ts: -------------------------------------------------------------------------------- 1 | import { fail } from '@noeldemartin/utils'; 2 | import type { Attributes } from 'soukai'; 3 | 4 | import type Recipe from '@/models/Recipe'; 5 | 6 | export default { 7 | 8 | createRecipe(attributes: Attributes, instructions: string[] = []): Cypress.Chainable { 9 | return cy.testingRuntime().then(runtime => runtime.createRecipe(attributes, instructions)); 10 | }, 11 | 12 | getRecipe(slug: string): Cypress.Chainable { 13 | return cy.testingRuntime().then(runtime => { 14 | const recipe = runtime.getRecipe(slug); 15 | 16 | recipe || fail(`Couldn't find recipe with '${slug}' slug`); 17 | 18 | return Promise.resolve(recipe as Recipe); 19 | }); 20 | }, 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /cypress/support/commands/forms.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | uploadFixture(label: string, filename: string): void { 4 | cy.fixtureBlob(filename).then(({ blob }) => { 5 | cy.contains('label', label).get<[HTMLInputElement]>('input[type="file"]').then(input => { 6 | const file = new File([blob], filename); 7 | const dataTransfer = new DataTransfer(); 8 | 9 | dataTransfer.items.add(file); 10 | 11 | input[0].files = dataTransfer.files; 12 | 13 | input[0].dispatchEvent(new Event('change')); 14 | }); 15 | }); 16 | }, 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /cypress/support/commands/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import type { TestingRuntime, TestingStartOptions } from '@/types/testing'; 2 | 3 | import { queueAuthenticatedRequests } from './auth'; 4 | 5 | function patchWindow(window: Window): void { 6 | window.prompt = (_, defaultAnswer) => defaultAnswer ?? null; 7 | } 8 | 9 | export default { 10 | 11 | testingRuntime(): Cypress.Chainable { 12 | return cy.window().its('testing').then(runtime => runtime as TestingRuntime); 13 | }, 14 | 15 | startApp(options: Partial = {}): void { 16 | cy.window() 17 | .then((window: Window) => { 18 | patchWindow(window); 19 | queueAuthenticatedRequests(window); 20 | 21 | return Cypress.Promise.cast(window.testing?.start(options)); 22 | }) 23 | .then(reloaded => { 24 | if (!reloaded) { 25 | return; 26 | } 27 | 28 | cy.press('Authorize'); 29 | cy.startApp(options); 30 | }); 31 | 32 | cy.service('$errors').then((Errors) => Errors.disable()); 33 | cy.contains('Something went wrong').should('not.exist'); 34 | }, 35 | 36 | waitForReload(options: Partial = {}): void { 37 | cy.get('#app.loading').then(() => cy.startApp(options)); 38 | }, 39 | 40 | waitTick(): void { 41 | cy.wait(50); 42 | }, 43 | 44 | }; 45 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | import { installChaiPlugin } from '@noeldemartin/solid-utils'; 2 | import installCustomCommands from '@cy/support/commands'; 3 | 4 | require('cypress-plugin-tab'); 5 | require('cypress-fill-command'); 6 | 7 | installChaiPlugin(); 8 | installCustomCommands(); 9 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2019", 5 | "types": [ 6 | "cypress" 7 | ], 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["../src/*"], 11 | "@cy/*": ["./*"] 12 | } 13 | }, 14 | "include": [ 15 | "./**/*.ts", 16 | "../src/**/*.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /cypress/types/chai.d.ts: -------------------------------------------------------------------------------- 1 | // TODO move to @noeldemartin/solid-utils 2 | declare namespace Chai { 3 | 4 | interface Assertion { 5 | turtle(triples: string): Assertion; 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /cypress/types/cypress-fill-command.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | fill(text: string): Chainable; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cypress/types/cypress-plugin-tab.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | tab(options?: Partial<{shift: Boolean}>): Chainable; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/docs/logo.png -------------------------------------------------------------------------------- /docs/secrets.md: -------------------------------------------------------------------------------- 1 | # Secrets 2 | 3 | Hey! How did you get here? 4 | 5 | There is a bunch of features that aren't documented, or even accessible from the UI. Eventually, I'd like to document them and make them accessible in the UI. But until then, I'm just leaving this list here. Let's see what you find out! 6 | 7 | - `/recipes/{slug}/history` 8 | - `/recipes/create?from={url}` 9 | - `/?autoReconnect=false` 10 | - `/?refreshProfile=true` 11 | -------------------------------------------------------------------------------- /docs/using-a-proxy.md: -------------------------------------------------------------------------------- 1 | # Using a Proxy 2 | 3 | As mentioned in the app, making requests to other domains is going to fail with a [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) error most of the time. Given that this application does not have a server component (it's running completely in the frontend), the only solution is to rely on an external service to work around that. 4 | 5 | And that's what the proxy url you can configure in the app is for. The app sends a request to a proxy server with the website url you want to parse, and gets back a response with the page HTML. The proxy url is expected to respond to a `POST` request with a payload containing a `url` field, and return the page HTML in the body. 6 | 7 | The proxy that comes configured with the app by default uses [noeldemartin/proxy](https://github.com/NoelDeMartin/proxy), so you're welcome to host that yourself. It doesn't track any ips nor personal information, so your requests should be anonymous anyways. But at the moment it's limited to 100 requests every 10 minutes (in total, not per user). 8 | 9 | Eventually, it may be better to use a proper [HTTP proxy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Proxy_servers_and_tunneling#http_tunneling). But I don't have a lot of experience working with proxies, and I didn't want to spend any more time working on this (I prefer to focus on UX and features). If you are interested in helping out with this, please [let me know](https://github.com/NoelDeMartin/umai/issues). 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { compilerOptions } = require('./tsconfig'); 2 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 3 | 4 | module.exports = { 5 | preset: 'ts-jest/presets/js-with-babel', 6 | testRegex: '\\.test\\.ts$', 7 | collectCoverageFrom: ['/src/**/*'], 8 | coveragePathIgnorePatterns: [ 9 | '/src/types/', 10 | '/src/main.ts', 11 | ], 12 | moduleFileExtensions: ['js', 'ts'], 13 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 14 | globals: { 15 | 'ts-jest': { 16 | babelConfig: { 17 | presets: [ 18 | ['@babel/preset-env', { targets: { node: '14' } }], 19 | ], 20 | }, 21 | }, 22 | }, 23 | setupFilesAfterEnv: ['/src/testing/setup.ts'], 24 | }; 25 | -------------------------------------------------------------------------------- /patches/image-blob-reduce+4.1.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/image-blob-reduce/dist/image-blob-reduce.esm.mjs b/node_modules/image-blob-reduce/dist/image-blob-reduce.esm.mjs 2 | index a3657de..e566c5f 100644 3 | --- a/node_modules/image-blob-reduce/dist/image-blob-reduce.esm.mjs 4 | +++ b/node_modules/image-blob-reduce/dist/image-blob-reduce.esm.mjs 5 | @@ -1208,9 +1208,7 @@ module.exports.can_use_canvas = function can_use_canvas(createCanvas) { 6 | d = null; 7 | d = ctx.getImageData(0, 0, 2, 1); 8 | 9 | - if (d.data[0] === 12 && d.data[1] === 23 && d.data[2] === 34 && d.data[3] === 255 && d.data[4] === 45 && d.data[5] === 56 && d.data[6] === 67 && d.data[7] === 255) { 10 | - usable = true; 11 | - } 12 | + usable = true; 13 | } catch (err) {} 14 | 15 | return usable; 16 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss': {}, 4 | 'autoprefixer': {}, 5 | 6 | // TODO only for storybook 7 | 'postcss-pseudo-classes': { 8 | blacklist: [], 9 | restrictTo: ['hover', 'focus-visible', 'focus'], 10 | allCombinations: true, 11 | preserveBeforeAfter: false, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/banner.png -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/favicon.ico -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /solid-server/README.md: -------------------------------------------------------------------------------- 1 | # Solid Server 2 | 3 | This folder is used to serve a [CSS](https://github.com/CommunitySolidServer/CommunitySolidServer) instance for testing and development. 4 | 5 | I previously had this dependency installed in the root project, but updating to `v5.0.0` from `v3.0.0` broke [Cypress](https://www.cypress.io/) showing a webpack error (And I thought my webpack days were behind me 😱️). After a couple of hours trying to fix it, I just decided to install this in a different folder and be done with it. 6 | 7 | At some point, I have to update the Cypress version as well and hopefully it's not using Webpack anymore. But I don't want to get into all of that now, so in the meantime this is a workaround that gets the job done. 8 | -------------------------------------------------------------------------------- /solid-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-server", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "@solid/community-server": "^5.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Black-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Black-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Black-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Black-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Bold-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Bold-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Bold-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Bold-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-ExtraLight-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-ExtraLight-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-ExtraLight-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-ExtraLight-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Black-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Black-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Black-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Black-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Bold-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Bold-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Bold-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Bold-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-ExtraLight-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-ExtraLight-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-ExtraLight-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-ExtraLight-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Light-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Light-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Light-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Light-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Medium-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Medium-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Medium-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Medium-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Normal-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Normal-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Normal-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Normal-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-SemiBold-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-SemiBold-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-SemiBold-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-SemiBold-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Thin-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Thin-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Italic-Thin-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Italic-Thin-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Light-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Light-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Light-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Light-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Medium-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Medium-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Medium-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Medium-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Normal-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Normal-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Normal-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Normal-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-SemiBold-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-SemiBold-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-SemiBold-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-SemiBold-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Thin-Latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Thin-Latin.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Livvic-Thin-LatinExt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/fonts/Livvic-Thin-LatinExt.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/README.md: -------------------------------------------------------------------------------- 1 | # Fonts 2 | 3 | Font files are included in the source code, instead of using an external CDN, because this app is designed to work offline-first without any external dependencies. 4 | 5 | These are the fonts used in the app: 6 | 7 | - [Livvic](https://github.com/Fonthausen/Livvic) (OFL-1.1 License) 8 | -------------------------------------------------------------------------------- /src/assets/icons/scanning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/solid-emblem.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/sounds/timer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoelDeMartin/umai/302a2b559c74d65c25ca2c4a7faa28d2c3e8aa2a/src/assets/sounds/timer.mp3 -------------------------------------------------------------------------------- /src/assets/styles/main.css: -------------------------------------------------------------------------------- 1 | @import './fonts.css'; 2 | @import './print.css'; 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | @layer base { 9 | 10 | html { 11 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 12 | } 13 | 14 | code { 15 | word-break: break-word; 16 | } 17 | 18 | } 19 | 20 | @layer utilities { 21 | 22 | .element-transitions-wrapper > .mx-edge { 23 | @apply mx-0; 24 | } 25 | 26 | .text-shadow { 27 | text-shadow: 0 2px 4px rgba(0, 0, 0, .9); 28 | } 29 | 30 | .text-shadow-transparent { 31 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0); 32 | } 33 | 34 | input[type=number].show-spinners::-webkit-inner-spin-button, 35 | input[type=number].show-spinners::-webkit-outer-spin-button { 36 | opacity: 1 37 | } 38 | 39 | } 40 | 41 | @layer components { 42 | 43 | .prose a:not(.core-link) { 44 | @apply no-underline hover:underline focus-visible:outline-none focus-visible:ring-2 text-primary-700 focus-visible:ring-primary-200 focus-visible:bg-primary-200; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/assets/styles/print.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | 3 | /* App styles */ 4 | 5 | #app-layout { 6 | --header-height: 0 !important; 7 | } 8 | 9 | .px-edge, .md\:px-edge { 10 | @apply px-0 !important; 11 | } 12 | 13 | .max-w-screen { 14 | @apply max-w-none !important; 15 | } 16 | 17 | .w-screen { 18 | @apply w-auto !important; 19 | } 20 | 21 | .h-screen { 22 | @apply h-auto !important; 23 | } 24 | 25 | /* @tailwindcss/typography styles */ 26 | 27 | .prose { 28 | --tw-prose-body: black !important; 29 | 30 | @apply max-w-none !important; 31 | } 32 | 33 | .prose :where(h2):not(:where([class~="not-prose"] *)) { 34 | @apply mb-1 !important; 35 | } 36 | 37 | .max-w-prose { 38 | @apply max-w-none !important; 39 | } 40 | 41 | /* Additional utilities styles */ 42 | 43 | .print\:text-no-shadow { 44 | text-shadow: none !important; 45 | } 46 | 47 | .print-bg { 48 | -webkit-print-color-adjust: exact !important; 49 | print-color-adjust: exact !important; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/boot.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset'; 2 | import type { App as VueApp } from 'vue'; 3 | 4 | import { bootstrapApplication } from '@/framework'; 5 | 6 | import './assets/styles/main.css'; 7 | import App from './App.vue'; 8 | import plugins from './plugins'; 9 | import routes, { registerAutoLinkingScopes, registerRouterBindings } from './routing'; 10 | import services, { registerServices } from './services'; 11 | 12 | interface BootOptions { 13 | beforeMount(app: VueApp): unknown | Promise; 14 | } 15 | 16 | export default async function boot({ beforeMount }: Partial = {}): Promise { 17 | await bootstrapApplication(App, { 18 | plugins, 19 | services, 20 | routes, 21 | beforeLaunch: () => registerServices(), 22 | async beforeMount(app) { 23 | registerAutoLinkingScopes(); 24 | registerRouterBindings(); 25 | 26 | await beforeMount?.(app); 27 | }, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/AppHeaderButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /src/components/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/components/AppOverlays.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 47 | -------------------------------------------------------------------------------- /src/components/AppPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/AppRouterView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | -------------------------------------------------------------------------------- /src/components/AppSnackbar.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 43 | -------------------------------------------------------------------------------- /src/components/base/BaseFileInput.ts: -------------------------------------------------------------------------------- 1 | export const enum BaseFileInputParseFormat { 2 | Json = 'json', 3 | Text = 'text', 4 | } 5 | -------------------------------------------------------------------------------- /src/components/core/CoreClipboard.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /src/components/core/CoreDetails.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/components/core/CoreFluidActionButton.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /src/components/core/CoreFluidInput.ts: -------------------------------------------------------------------------------- 1 | import type { IFocusable } from '@/framework/components/headless'; 2 | 3 | export default interface ICoreFluidInput extends IFocusable { 4 | minWidth: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/core/CoreFluidInputListItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 45 | -------------------------------------------------------------------------------- /src/components/core/CoreFluidTextAreaListItem.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | -------------------------------------------------------------------------------- /src/components/core/CoreForm.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/core/CoreFormErrors.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /src/components/core/CoreInputSubmit.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 41 | -------------------------------------------------------------------------------- /src/components/core/CoreJSON.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/components/core/CoreProseFiller.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/core/CoreSelect.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /src/components/core/CoreSortableList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | -------------------------------------------------------------------------------- /src/components/core/index.ts: -------------------------------------------------------------------------------- 1 | export enum CoreColor { 2 | Primary = 'primary', 3 | Solid = 'brand-solid', 4 | Danger = 'danger', 5 | } 6 | 7 | export enum CoreAlignment { 8 | Start = 'start', 9 | Center = 'center', 10 | End = 'end', 11 | } 12 | -------------------------------------------------------------------------------- /src/components/core/lists/CoreListItemValue.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from '@noeldemartin/utils'; 2 | import type { Constructor } from '@noeldemartin/utils'; 3 | 4 | import FormValue from '@/framework/forms/FormValue'; 5 | 6 | export default class CoreListItemValue extends FormValue { 7 | 8 | public readonly id: string; 9 | 10 | constructor(value: string = '', id?: string) { 11 | super(value); 12 | 13 | this.id = id ?? uuid(); 14 | } 15 | 16 | public update(value: string): this { 17 | const Constructor = this.constructor as unknown as Constructor; 18 | 19 | return new Constructor(value, this.id); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/components/modals/AppLoadingModal.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/components/modals/AppLoginModal.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 46 | -------------------------------------------------------------------------------- /src/components/modals/AppModal.transitions.ts: -------------------------------------------------------------------------------- 1 | import { fadeIn, fadeOut, scaleDown, scaleUp } from '@/framework/utils/transitions'; 2 | import { requireChildElement } from '@/framework/utils/dom'; 3 | 4 | export async function hideModal($root: HTMLElement, duration: number): Promise { 5 | const $panel = requireChildElement($root, '.app-modal--panel'); 6 | 7 | await Promise.all([ 8 | fadeOut($panel, { duration, easing: 'ease-in' }), 9 | scaleDown($panel, { duration, easing: 'ease-in' }), 10 | ]); 11 | 12 | $root.classList.add('opacity-0'); 13 | $root.classList.add('pointer-events-none'); 14 | } 15 | 16 | export async function showModal($root: HTMLElement, duration: number): Promise { 17 | const $panel = requireChildElement($root, '.app-modal--panel'); 18 | 19 | $root.classList.remove('opacity-0'); 20 | $root.classList.remove('pointer-events-none'); 21 | 22 | await Promise.all([ 23 | fadeIn($panel, { duration, easing: 'ease-out' }), 24 | scaleUp($panel, { duration, easing: 'ease-out' }), 25 | ]); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/modals/AppModal.ts: -------------------------------------------------------------------------------- 1 | export default interface IAppModal { 2 | close(result: Result): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/modals/AppSnackbars.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /src/components/modals/CloudStatusModal.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/modals/CreateRecipeModal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/modals/WebImportHtmlModal.ts: -------------------------------------------------------------------------------- 1 | import type { ModalComponent } from '@/framework/core/services/UIService'; 2 | 3 | export type WebImportHtmlModalComponent = ModalComponent<{ url: string }, { html: string }>; 4 | -------------------------------------------------------------------------------- /src/components/modals/WebImportHtmlModal.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /src/components/modals/WebImportModalAdvancedOptions.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 40 | -------------------------------------------------------------------------------- /src/components/modals/WebImportModalResultsRecipe.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 38 | -------------------------------------------------------------------------------- /src/components/modals/WebImportModalResultsRecipeRadio.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /src/components/modals/WebImportModalStart.ts: -------------------------------------------------------------------------------- 1 | export default interface IWebImportModalStart { 2 | submit(): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/modals/WebImportModalStart.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | -------------------------------------------------------------------------------- /src/components/modals/WebImportScanning.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/recipe/RecipePage.ts: -------------------------------------------------------------------------------- 1 | export default interface IRecipePage { 2 | showPrimaryPanel(): Promise; 3 | showSecondaryPanel(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/recipe/RecipePageMetadata.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/recipe/RecipeShareOptions.ts: -------------------------------------------------------------------------------- 1 | export enum RecipeShareOption { 2 | Umai = 'umai', 3 | Solid = 'solid', 4 | JsonLD = 'jsonld', 5 | Print = 'print', 6 | } 7 | -------------------------------------------------------------------------------- /src/components/recipe/RecipeTitle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/components/recipe/RecipesGrid.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | -------------------------------------------------------------------------------- /src/directives/wobbly-border.ts: -------------------------------------------------------------------------------- 1 | import { randomInt, range } from '@noeldemartin/utils'; 2 | 3 | import { defineDirective } from '@/framework/utils/vue'; 4 | 5 | function randomBorderRadius(corner: Corner, options: WobblyBorderOptions): string { 6 | const { min, max, ...corners } = options; 7 | 8 | return corners[corner] ? `${randomInt(min, max)}px` : '0'; 9 | } 10 | 11 | export const sortedCorners = ['topLeft', 'topRight', 'bottomRight', 'bottomLeft'] as const; 12 | 13 | export type Corner = typeof sortedCorners[number]; 14 | 15 | export type WobblyBorderOptions = Record & { 16 | min: number; 17 | max: number; 18 | }; 19 | 20 | export function randomWobblyBorderRadius(options: Partial = {}): string { 21 | const fullOptions = { 22 | min: 50, 23 | max: 150, 24 | topLeft: true, 25 | topRight: true, 26 | bottomRight: true, 27 | bottomLeft: true, 28 | ...options, 29 | }; 30 | 31 | return range(2) 32 | .map(() => sortedCorners.map(corner => randomBorderRadius(corner, fullOptions)).join(' ')) 33 | .join(' / '); 34 | } 35 | 36 | export default defineDirective({ 37 | mounted(element: HTMLElement, { value }) { 38 | if (value === false) return; 39 | if (value === true || !value) value = {}; 40 | 41 | element.style.borderRadius = randomWobblyBorderRadius(value); 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/errors/TooManyRequestsError.ts: -------------------------------------------------------------------------------- 1 | import { JSError } from '@noeldemartin/utils'; 2 | 3 | export default class TooManyRequestsError extends JSError { 4 | 5 | constructor(public url: string, public proxyUrl: string) { 6 | super(`Too many requests for ${url}`); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/framework/auth/Authenticator.mock.ts: -------------------------------------------------------------------------------- 1 | import { SolidEngine } from 'soukai-solid'; 2 | import type { Engine } from 'soukai'; 3 | import type { Fetch } from '@noeldemartin/solid-utils'; 4 | 5 | import type Authenticator from './Authenticator'; 6 | 7 | type MockedMethods = 'requireAuthenticatedFetch'; 8 | 9 | export default class AuthenticatorMock implements Pick { 10 | 11 | public engine: Engine; 12 | private fetch: Fetch; 13 | 14 | constructor(fetch: Fetch) { 15 | this.fetch = fetch; 16 | this.engine = new SolidEngine(fetch); 17 | } 18 | 19 | public requireAuthenticatedFetch(): Fetch { 20 | return this.fetch; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/framework/auth/errors/AuthenticationCancelledError.ts: -------------------------------------------------------------------------------- 1 | import { JSError } from '@noeldemartin/utils'; 2 | 3 | export default class AuthenticationCancelledError extends JSError {} 4 | -------------------------------------------------------------------------------- /src/framework/auth/index.ts: -------------------------------------------------------------------------------- 1 | import LocalStorageAuthenticator from './authenticators/LocalStorageAuthenticator'; 2 | import InruptAuthenticator from './authenticators/InruptAuthenticator'; 3 | import type Authenticator from './Authenticator'; 4 | 5 | const _authenticators = {} as Authenticators; 6 | 7 | type BaseAuthenticators = typeof authenticators; 8 | 9 | export const authenticators = { 10 | localStorage: new LocalStorageAuthenticator, 11 | inrupt: new InruptAuthenticator, 12 | }; 13 | 14 | export function setDefaultAuthenticator(authenticator: Authenticator): void { 15 | _authenticators.default = authenticator; 16 | } 17 | 18 | export function getAuthenticator(name: T): Authenticators[T] { 19 | return _authenticators[name]; 20 | } 21 | 22 | export function registerAuthenticator(name: T, authenticator: Authenticators[T]): void { 23 | _authenticators[name] = authenticator; 24 | 25 | authenticator.name = authenticator.name ?? name; 26 | } 27 | 28 | export type AuthenticatorName = keyof Authenticators; 29 | 30 | export interface Authenticators extends BaseAuthenticators { 31 | default: Authenticator; 32 | } 33 | -------------------------------------------------------------------------------- /src/framework/components/ConfirmModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /src/framework/components/ErrorReport.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /src/framework/components/LoadingModal.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /src/framework/components/MarkdownModal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/framework/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 39 | -------------------------------------------------------------------------------- /src/framework/components/headless/HeadlessButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 59 | -------------------------------------------------------------------------------- /src/framework/components/headless/HeadlessForm.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /src/framework/components/headless/HeadlessInput.d.ts: -------------------------------------------------------------------------------- 1 | import type { IFocusable } from '@/framework/components/headless'; 2 | 3 | export default interface IHeadlessInput extends IFocusable { 4 | value: T; 5 | hasErrors: boolean; 6 | } 7 | 8 | export interface HeadlessInputController { 9 | id: string; 10 | type: string; 11 | value: unknown | null; 12 | disabled: boolean; 13 | name?: string; 14 | placeholder?: string; 15 | error?: string | null; 16 | inputElement?: HTMLInputElement | HTMLTextAreaElement; 17 | 18 | update(): void; 19 | } 20 | -------------------------------------------------------------------------------- /src/framework/components/headless/HeadlessInputError.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/framework/components/headless/HeadlessInputInput.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /src/framework/components/headless/HeadlessInputLabel.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/framework/components/headless/HeadlessInputTextArea.vue: -------------------------------------------------------------------------------- 1 |