├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── .vscode └── extensions.json ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── auth.config.js ├── schema.ts ├── todos.ts └── tsconfig.json ├── env.d.ts ├── index.html ├── package.json ├── postcss.config.cjs ├── public ├── favicon.ico └── icon │ ├── icon-192x192.png │ └── icon-512x512.png ├── src ├── App.vue ├── api.ts ├── components │ ├── AddTodoForm.vue │ ├── DarkModeToggle.vue │ ├── ServiceWorkerPrompt.vue │ ├── Todo.vue │ ├── TodoListPaginated.vue │ ├── convex │ │ ├── EnsureAuthenticated.vue │ │ ├── PaginatedQuery.vue │ │ ├── PaginatedQueryInner.vue │ │ ├── Query.vue │ │ └── QueryInner.vue │ └── ui │ │ ├── UiIcon.vue │ │ ├── UiSpinner.vue │ │ ├── buttons │ │ ├── UiButton.vue │ │ ├── UiButtonBase.vue │ │ ├── UiGhostButton.vue │ │ ├── UiIconButton.vue │ │ └── UiLinkButton.vue │ │ ├── drawer │ │ ├── UiDrawer.vue │ │ ├── UiDrawerContent.vue │ │ ├── UiDrawerHeader.vue │ │ └── UiSimpleDrawer.vue │ │ ├── forms │ │ ├── UiFormControl.vue │ │ ├── UiFormError.vue │ │ └── UiFormLabel.vue │ │ ├── inputs │ │ ├── UiCheckbox.vue │ │ ├── UiSwitch.vue │ │ └── UiTextInput.vue │ │ └── modal │ │ ├── UiModal.vue │ │ ├── UiModalContent.vue │ │ ├── UiModalHeader.vue │ │ └── UiSimpleModal.vue ├── composables │ ├── convex │ │ ├── useAction.ts │ │ ├── useConvex.ts │ │ ├── useMutation.ts │ │ ├── usePaginatedQuery.ts │ │ ├── useQueries.ts │ │ ├── useQuery.ts │ │ └── useSuspenseQuery.ts │ ├── useFocusOn.ts │ ├── useSafeInject.ts │ └── useStyles.ts ├── directives │ └── vFocusOn.ts ├── main.ts ├── pages │ ├── index.vue │ └── profile │ │ └── [id].vue ├── plugins │ └── convex.ts ├── styles │ ├── global.css │ ├── reset.css │ ├── theme.css │ └── utils.css ├── sw.ts └── utils │ ├── assertions.ts │ ├── convex │ └── QueriesObserver.ts │ ├── dom.ts │ └── types.ts ├── stylelint.config.js ├── tools ├── ark-ui-resolver.ts └── uno-openprops-preset.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── typed-router.d.ts ├── uno.config.ts ├── vite.config.ts └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-recommended', 8 | '@vue/eslint-config-typescript/recommended', 9 | '@vue/eslint-config-prettier/skip-formatting', 10 | 'eslint:recommended' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | }, 15 | rules: { 16 | 'no-undef': 'off', 17 | 'no-redeclare': 'off', 18 | 'no-unused-vars': 'off', 19 | 'vue/multi-word-component-names': 'off', 20 | 'vue/no-setup-props-destructure': 'off', 21 | '@typescript-eslint/ban-ts-comment': 'off', 22 | '@typescript-eslint/no-non-null-assertion': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "htmlWhitespaceSensitivity": "ignore", 4 | "semi": true, 5 | "tabWidth": 2, 6 | "singleQuote": true, 7 | "printWidth": 90, 8 | "trailingComma": "none", 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "Vue.vscode-typescript-vue-plugin", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue + Vite + Convex 2 | 3 | Template to easily use [Convex](https://www.convex.dev/) with [Vue](https://vuejs.org/) and [Auth0](https://auth0.com/) 4 | 5 | Everything should be setup to work properly. 6 | 7 | [Click here to go to the deployed app](https://vue-convex-example.vercel.app/) 8 | 9 | **This branch adds a bunch of goodies that I like to use for my vue projects and can be fairly opinionated, if you're only interested in the convex stuff, check the [minimal branch](https://github.com/loicpennequin/convex-vue-vite-template/tree/minimal)** 10 | 11 | - A `createConvex` vue plugin has been created to instanciated a slightly modified version of the `ConvexReactClient`. 12 | - If `auth0` options are provided to the plugin, navigation guards will be added to the application. You can tweak the option to have a different redirect url, or more involved way of determining if an autentication check should be made (by default, add `needsauth: true` to a [route block meta](https://github.com/posva/unplugin-vue-router#sfc-route-custom-block)). 13 | - ⚠️ You will need to add your auth0 credentials (domain and client ID) 14 | - in a .env.local file under the keys `VITE_AUTH0_DOMAIN` and `VITE_AUTH0_CLIENTID`. You can use different names but will have ti make the appropriate changes in `src/main.ts` 15 | - as environment variables through the convex dashboard ([see here](https://docs.convex.dev/production/environment-variables)) under the keys `AUTH0_DOMAIN` and `AUTH0_APPLICATIONID`. You can use different names but will have ti make the appropriate changes in `convex/auh.config.js` 16 | 17 | 🧪 - Experimental: might have bugs 18 | 🔨 - Available soon 19 | 20 | ## Composables 21 | 22 | ### `useQuery` 23 | 24 | ```html 25 | 42 | 43 | 48 | ``` 49 | 50 | ### `useSuspenseQuery` 51 | 52 | like useQuery but can be awaited and will resolve once the query result is available (either from cache or from a network call). This enables you to use this composable in conjuction with Vue's [``](https://vuejs.org/guide/built-ins/suspense.html). Note: like useQuery, the value will be reactive and will update automatically when it's value changes on the Convex server. 53 | 54 | ```html 55 | 75 | 76 | 82 | ``` 83 | 84 | ### 🧪`usePaginatedQuery` 85 | 86 | ```html 87 | 101 | 102 | 111 | ``` 112 | 113 | ### `useMutation` 114 | 115 | Now with optimistic updates ! (🧪) 116 | 117 | ```html 118 | 147 | 148 | 155 | ``` 156 | 157 | ### `useAction` 158 | 159 | ```html 160 | 165 | 166 | 169 | ``` 170 | 171 | ### `useConvex` 172 | 173 | if you need to use the ConvexVueClient directly. You probably don't need it. 174 | 175 | ### `useConvexAuth` 176 | 177 | if you used the `auth0` option in the plugin, it will return you the loading and authenticated state. For additional auth utilities like login, logout, user etc, please use `useAuth0` from `@auth0/auth0-vue` 178 | 179 | ```html 180 | 183 | ``` 184 | 185 | ## Components 186 | 187 | ### `` 188 | 189 | Allows you to display content depending on convex Auth status. ⚠️ you need the autho0 options in the convexPlugin for this component to work. 190 | It accepts the following slots: 191 | 192 | - default: when the user is logged in 193 | - fallback: when the user isn't logged in 194 | - loading: when the authentication process is pending 195 | 196 | ```html 197 | 200 | 201 | 213 | ``` 214 | 215 | ### `` 216 | 217 | Allows you to display different UI during the lifecycle of a convex query. 218 | It takes the following props 219 | 220 | - `query`: a function taking the convex api as a parameter and returning a query, eg: `:query="api => api.messages.list"` 221 | - `args`: the arguments for the returned query 222 | 223 | It accepts the following slots: 224 | 225 | - default: should be used to display the query results (avaiable via [scoped slots](https://vuejs.org/guide/components/slots.html#scoped-slots)) 226 | - loading: pending state when the query is loading for the first time 227 | - error: should be used to displau an error UI. It takes the error as slot props, as well as a `clearError` function to clear the underlying error b boundary (note: doing so will retry the query). 228 | 229 | It will also emit the error when / if it happens. 230 | 231 | ```html 232 | 235 | 236 | 237 | 238 | 239 | 244 | 245 | 248 | 249 | ``` 250 | 251 | ### 🧪 `` 252 | 253 | Similar to ``, but handles pagination 254 | 255 | ```html 256 | 259 | 260 | 288 | ``` 289 | 290 | ## Additional features 291 | 292 | This branch adds a lot of other stuff for better DX and is overall more batteries included. It's fairly opinionated so if you just want to use convex with vue check out the `minimal` branch 293 | 294 | - [Auto import](https://github.com/unplugin/unplugin-auto-import) for vue, @vueuse/core, vee-validate, vue-router, as well as the `composables` and `utils` directories 295 | - [Auto imported vue components](https://github.com/unplugin/unplugin-vue-components) in the `components` directory, as well asall components of the [Ark-ui](https://ark-ui.com/docs/vue/overview/introduction) library under the `Ark` prefix, eg. `ArkAccordion` 296 | - [Open-props](https://open-props.style/), as well as [UnoCSS](https://unocss.dev/) for styling. A preset a custom UNO theme has been added to use open-props' values. The uno config also scans your `src/styles/theme.css` file to add additional colors, see the file for more informations 297 | - [vee-validate](https://vee-validate.logaretm.com/v4/) for form management 298 | - A few UI components for buttons, inputs etc...nothing fancy 299 | - [Iconify component](https://iconify.design/docs/icon-components/vue/) (UiIcon) to easily add an icon from inconify. This can be used instead of uno's `presetIcons` when you want to be able to pass an icon name as props for example. 300 | - [PWA Support](https://vite-pwa-org.netlify.app/). ⚠️You...might wanna make sure you replace the icons inèpublic/icons, as I wil be forever 15 years old. 301 | - A few utility types have been added in `src/utils/types` 302 | - A theme supporting dark mode, and a dark mode component 303 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const ConvexVueClient: (typeof import('./src/utils/convex'))['ConvexVueClient'] 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const FieldContextKey: typeof import('vee-validate')['FieldContextKey'] 11 | const FormContextKey: typeof import('vee-validate')['FormContextKey'] 12 | const HasDataLoaderMeta: (typeof import('./typed-router.d'))['HasDataLoaderMeta'] 13 | const QueriesObserver: typeof import('./src/utils/convex/QueriesObserver')['QueriesObserver'] 14 | const RouterLink: (typeof import('./typed-router.d'))['RouterLink'] 15 | const RouterLinkProps: (typeof import('./typed-router.d'))['RouterLinkProps'] 16 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] 17 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] 18 | const computed: typeof import('vue')['computed'] 19 | const computedAsync: typeof import('@vueuse/core')['computedAsync'] 20 | const computedEager: typeof import('@vueuse/core')['computedEager'] 21 | const computedInject: typeof import('@vueuse/core')['computedInject'] 22 | const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] 23 | const configure: typeof import('vee-validate')['configure'] 24 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] 25 | const controlledRef: typeof import('@vueuse/core')['controlledRef'] 26 | const convexPlugin: (typeof import('./src/utils/convex'))['convexPlugin'] 27 | const createApp: typeof import('vue')['createApp'] 28 | const createConvex: (typeof import('./src/composables/convex'))['createConvex'] 29 | const createEventHook: typeof import('@vueuse/core')['createEventHook'] 30 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] 31 | const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] 32 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] 33 | const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] 34 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] 35 | const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] 36 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 37 | const customRef: typeof import('vue')['customRef'] 38 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 39 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] 40 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 41 | const defineComponent: typeof import('vue')['defineComponent'] 42 | const defineLoader: typeof import('vue-router/auto')['defineLoader'] 43 | const definePage: typeof import('unplugin-vue-router/runtime')['_definePage'] 44 | const defineRule: typeof import('vee-validate')['defineRule'] 45 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] 46 | const effectScope: typeof import('vue')['effectScope'] 47 | const extendRef: typeof import('@vueuse/core')['extendRef'] 48 | const focusEmitter: typeof import('./src/composables/useFocusOn')['focusEmitter'] 49 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 50 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 51 | const getFocusableChildren: typeof import('./src/utils/dom')['getFocusableChildren'] 52 | const getStyle: typeof import('./src/utils/theme-helpers')['getStyle'] 53 | const h: typeof import('vue')['h'] 54 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 55 | const inject: typeof import('vue')['inject'] 56 | const isBoolean: typeof import('./src/utils/assertions')['isBoolean'] 57 | const isDefined: typeof import('./src/utils/assertions')['isDefined'] 58 | const isNever: typeof import('./src/utils/assertions')['isNever'] 59 | const isNumber: typeof import('./src/utils/assertions')['isNumber'] 60 | const isObject: typeof import('./src/utils/assertions')['isObject'] 61 | const isProxy: typeof import('vue')['isProxy'] 62 | const isReactive: typeof import('vue')['isReactive'] 63 | const isReadonly: typeof import('vue')['isReadonly'] 64 | const isRef: typeof import('vue')['isRef'] 65 | const isString: typeof import('./src/utils/assertions')['isString'] 66 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 67 | const markRaw: typeof import('vue')['markRaw'] 68 | const nextTick: typeof import('vue')['nextTick'] 69 | const onActivated: typeof import('vue')['onActivated'] 70 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 71 | const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave'] 72 | const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate'] 73 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 74 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 75 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 76 | const onDeactivated: typeof import('vue')['onDeactivated'] 77 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 78 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] 79 | const onLongPress: typeof import('@vueuse/core')['onLongPress'] 80 | const onMounted: typeof import('vue')['onMounted'] 81 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 82 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 83 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 84 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 85 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] 86 | const onUnmounted: typeof import('vue')['onUnmounted'] 87 | const onUpdated: typeof import('vue')['onUpdated'] 88 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] 89 | const provide: typeof import('vue')['provide'] 90 | const reactify: typeof import('@vueuse/core')['reactify'] 91 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] 92 | const reactive: typeof import('vue')['reactive'] 93 | const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] 94 | const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] 95 | const reactivePick: typeof import('@vueuse/core')['reactivePick'] 96 | const readonly: typeof import('vue')['readonly'] 97 | const ref: typeof import('vue')['ref'] 98 | const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] 99 | const refDebounced: typeof import('@vueuse/core')['refDebounced'] 100 | const refDefault: typeof import('@vueuse/core')['refDefault'] 101 | const refThrottled: typeof import('@vueuse/core')['refThrottled'] 102 | const refWithControl: typeof import('@vueuse/core')['refWithControl'] 103 | const resolveComponent: typeof import('vue')['resolveComponent'] 104 | const resolveRef: typeof import('@vueuse/core')['resolveRef'] 105 | const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 106 | const setupDataFetchingGuard: (typeof import('./typed-router.d'))['setupDataFetchingGuard'] 107 | const shallowReactive: typeof import('vue')['shallowReactive'] 108 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 109 | const shallowRef: typeof import('vue')['shallowRef'] 110 | const stopDataFetchingScope: (typeof import('./typed-router.d'))['stopDataFetchingScope'] 111 | const styleBuilder: typeof import('./src/utils/theme-helpers')['styleBuilder'] 112 | const syncRef: typeof import('@vueuse/core')['syncRef'] 113 | const syncRefs: typeof import('@vueuse/core')['syncRefs'] 114 | const templateRef: typeof import('@vueuse/core')['templateRef'] 115 | const throttledRef: typeof import('@vueuse/core')['throttledRef'] 116 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] 117 | const toRaw: typeof import('vue')['toRaw'] 118 | const toReactive: typeof import('@vueuse/core')['toReactive'] 119 | const toRef: typeof import('vue')['toRef'] 120 | const toRefs: typeof import('vue')['toRefs'] 121 | const toValue: typeof import('vue')['toValue'] 122 | const triggerRef: typeof import('vue')['triggerRef'] 123 | const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] 124 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] 125 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 126 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 127 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 128 | const unoConfig: (typeof import('./uno.config'))['default'] 129 | const unref: typeof import('vue')['unref'] 130 | const unrefElement: typeof import('@vueuse/core')['unrefElement'] 131 | const until: typeof import('@vueuse/core')['until'] 132 | const useAction: typeof import('./src/composables/convex/useAction')['useAction'] 133 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] 134 | const useAnimate: typeof import('@vueuse/core')['useAnimate'] 135 | const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] 136 | const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] 137 | const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] 138 | const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] 139 | const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] 140 | const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] 141 | const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] 142 | const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] 143 | const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] 144 | const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] 145 | const useArraySome: typeof import('@vueuse/core')['useArraySome'] 146 | const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] 147 | const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] 148 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] 149 | const useAttrs: typeof import('vue')['useAttrs'] 150 | const useAuth0: typeof import('@auth0/auth0-vue')['useAuth0'] 151 | const useBase64: typeof import('@vueuse/core')['useBase64'] 152 | const useBattery: typeof import('@vueuse/core')['useBattery'] 153 | const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] 154 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] 155 | const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] 156 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] 157 | const useCached: typeof import('@vueuse/core')['useCached'] 158 | const useClipboard: typeof import('@vueuse/core')['useClipboard'] 159 | const useCloned: typeof import('@vueuse/core')['useCloned'] 160 | const useColorMode: typeof import('@vueuse/core')['useColorMode'] 161 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] 162 | const useConvex: typeof import('./src/composables/convex/useConvex')['useConvex'] 163 | const useConvexAuth: typeof import('./src/composables/convex/useConvex')['useConvexAuth'] 164 | const useConvexAuth0Provider: (typeof import('./src/composables/convex-auth'))['useConvexAuth0Provider'] 165 | const useConvexAuthProvider: (typeof import('./src/utils/convex'))['useConvexAuthProvider'] 166 | const useConvexQuery: (typeof import('./src/utils/convex'))['useConvexQuery'] 167 | const useCounter: typeof import('@vueuse/core')['useCounter'] 168 | const useCssModule: typeof import('vue')['useCssModule'] 169 | const useCssVar: typeof import('@vueuse/core')['useCssVar'] 170 | const useCssVars: typeof import('vue')['useCssVars'] 171 | const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] 172 | const useCycleList: typeof import('@vueuse/core')['useCycleList'] 173 | const useDark: typeof import('@vueuse/core')['useDark'] 174 | const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] 175 | const useDebounce: typeof import('@vueuse/core')['useDebounce'] 176 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] 177 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] 178 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] 179 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] 180 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] 181 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] 182 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] 183 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] 184 | const useDraggable: typeof import('@vueuse/core')['useDraggable'] 185 | const useDropZone: typeof import('@vueuse/core')['useDropZone'] 186 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] 187 | const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] 188 | const useElementHover: typeof import('@vueuse/core')['useElementHover'] 189 | const useElementSize: typeof import('@vueuse/core')['useElementSize'] 190 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] 191 | const useEventBus: typeof import('@vueuse/core')['useEventBus'] 192 | const useEventListener: typeof import('@vueuse/core')['useEventListener'] 193 | const useEventSource: typeof import('@vueuse/core')['useEventSource'] 194 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 195 | const useFavicon: typeof import('@vueuse/core')['useFavicon'] 196 | const useFetch: typeof import('@vueuse/core')['useFetch'] 197 | const useField: typeof import('vee-validate')['useField'] 198 | const useFieldArray: typeof import('vee-validate')['useFieldArray'] 199 | const useFieldError: typeof import('vee-validate')['useFieldError'] 200 | const useFieldValue: typeof import('vee-validate')['useFieldValue'] 201 | const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 202 | const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 203 | const useFocus: typeof import('@vueuse/core')['useFocus'] 204 | const useFocusOn: typeof import('./src/composables/useFocusOn')['useFocusOn'] 205 | const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] 206 | const useForm: typeof import('vee-validate')['useForm'] 207 | const useFormErrors: typeof import('vee-validate')['useFormErrors'] 208 | const useFormValues: typeof import('vee-validate')['useFormValues'] 209 | const useFps: typeof import('@vueuse/core')['useFps'] 210 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] 211 | const useGamepad: typeof import('@vueuse/core')['useGamepad'] 212 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] 213 | const useIdle: typeof import('@vueuse/core')['useIdle'] 214 | const useImage: typeof import('@vueuse/core')['useImage'] 215 | const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] 216 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] 217 | const useInterval: typeof import('@vueuse/core')['useInterval'] 218 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] 219 | const useIsFieldDirty: typeof import('vee-validate')['useIsFieldDirty'] 220 | const useIsFieldTouched: typeof import('vee-validate')['useIsFieldTouched'] 221 | const useIsFieldValid: typeof import('vee-validate')['useIsFieldValid'] 222 | const useIsFormDirty: typeof import('vee-validate')['useIsFormDirty'] 223 | const useIsFormTouched: typeof import('vee-validate')['useIsFormTouched'] 224 | const useIsFormValid: typeof import('vee-validate')['useIsFormValid'] 225 | const useIsSubmitting: typeof import('vee-validate')['useIsSubmitting'] 226 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] 227 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] 228 | const useLink: (typeof import('./typed-router.d'))['useLink'] 229 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] 230 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] 231 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] 232 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] 233 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] 234 | const useMemoize: typeof import('@vueuse/core')['useMemoize'] 235 | const useMemory: typeof import('@vueuse/core')['useMemory'] 236 | const useMounted: typeof import('@vueuse/core')['useMounted'] 237 | const useMouse: typeof import('@vueuse/core')['useMouse'] 238 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] 239 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] 240 | const useMutation: typeof import('./src/composables/convex/useMutation')['useMutation'] 241 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] 242 | const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 243 | const useNetwork: typeof import('@vueuse/core')['useNetwork'] 244 | const useNow: typeof import('@vueuse/core')['useNow'] 245 | const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 246 | const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 247 | const useOnline: typeof import('@vueuse/core')['useOnline'] 248 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] 249 | const usePaginatedQuery: typeof import('./src/composables/convex/usePaginatedQuery')['usePaginatedQuery'] 250 | const useParallax: typeof import('@vueuse/core')['useParallax'] 251 | const useParentElement: typeof import('@vueuse/core')['useParentElement'] 252 | const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] 253 | const usePermission: typeof import('@vueuse/core')['usePermission'] 254 | const usePointer: typeof import('@vueuse/core')['usePointer'] 255 | const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] 256 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] 257 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] 258 | const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] 259 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] 260 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] 261 | const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] 262 | const usePrevious: typeof import('@vueuse/core')['usePrevious'] 263 | const useQueries: typeof import('./src/composables/convex/useQueries')['useQueries'] 264 | const useQueriesHelper: typeof import('./src/composables/convex/useQueries')['useQueriesHelper'] 265 | const useQuery: typeof import('./src/composables/convex/useQuery')['useQuery'] 266 | const useRafFn: typeof import('@vueuse/core')['useRafFn'] 267 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] 268 | const useResetForm: typeof import('vee-validate')['useResetForm'] 269 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] 270 | const useRoute: typeof import('vue-router/auto')['useRoute'] 271 | const useRouter: typeof import('vue-router/auto')['useRouter'] 272 | const useSafeInject: typeof import('./src/composables/useSafeInject')['useSafeInject'] 273 | const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] 274 | const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] 275 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] 276 | const useScroll: typeof import('@vueuse/core')['useScroll'] 277 | const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] 278 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] 279 | const useShare: typeof import('@vueuse/core')['useShare'] 280 | const useSlots: typeof import('vue')['useSlots'] 281 | const useSorted: typeof import('@vueuse/core')['useSorted'] 282 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 283 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 284 | const useStepper: typeof import('@vueuse/core')['useStepper'] 285 | const useStorage: typeof import('@vueuse/core')['useStorage'] 286 | const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 287 | const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 288 | const useStyles: typeof import('./src/composables/useStyles')['useStyles'] 289 | const useSubmitCount: typeof import('vee-validate')['useSubmitCount'] 290 | const useSubmitForm: typeof import('vee-validate')['useSubmitForm'] 291 | const useSupported: typeof import('@vueuse/core')['useSupported'] 292 | const useSuspenseQuery: typeof import('./src/composables/convex/useSuspenseQuery')['useSuspenseQuery'] 293 | const useSwipe: typeof import('@vueuse/core')['useSwipe'] 294 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 295 | const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] 296 | const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 297 | const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 298 | const useThrottle: typeof import('@vueuse/core')['useThrottle'] 299 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 300 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] 301 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] 302 | const useTimeout: typeof import('@vueuse/core')['useTimeout'] 303 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] 304 | const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] 305 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] 306 | const useTitle: typeof import('@vueuse/core')['useTitle'] 307 | const useToNumber: typeof import('@vueuse/core')['useToNumber'] 308 | const useToString: typeof import('@vueuse/core')['useToString'] 309 | const useToggle: typeof import('@vueuse/core')['useToggle'] 310 | const useTransition: typeof import('@vueuse/core')['useTransition'] 311 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] 312 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] 313 | const useVModel: typeof import('@vueuse/core')['useVModel'] 314 | const useVModels: typeof import('@vueuse/core')['useVModels'] 315 | const useValidateField: typeof import('vee-validate')['useValidateField'] 316 | const useValidateForm: typeof import('vee-validate')['useValidateForm'] 317 | const useVibrate: typeof import('@vueuse/core')['useVibrate'] 318 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] 319 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] 320 | const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] 321 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] 322 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] 323 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] 324 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] 325 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 326 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 327 | const validate: typeof import('vee-validate')['validate'] 328 | const viteConfig: (typeof import('./vite.config'))['default'] 329 | const watch: typeof import('vue')['watch'] 330 | const watchArray: typeof import('@vueuse/core')['watchArray'] 331 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 332 | const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 333 | const watchDeep: typeof import('@vueuse/core')['watchDeep'] 334 | const watchEffect: typeof import('vue')['watchEffect'] 335 | const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] 336 | const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] 337 | const watchOnce: typeof import('@vueuse/core')['watchOnce'] 338 | const watchPausable: typeof import('@vueuse/core')['watchPausable'] 339 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 340 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 341 | const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 342 | const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 343 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 344 | const whenever: typeof import('@vueuse/core')['whenever'] 345 | } 346 | // for type re-export 347 | declare global { 348 | // @ts-ignore 349 | export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' 350 | } 351 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AddTodoForm: typeof import('./src/components/AddTodoForm.vue')['default'] 11 | ArkCheckbox: typeof import('@ark-ui/vue')['Checkbox'] 12 | ArkCheckboxControl: typeof import('@ark-ui/vue')['CheckboxControl'] 13 | ArkCheckboxLabel: typeof import('@ark-ui/vue')['CheckboxLabel'] 14 | ArkDialog: typeof import('@ark-ui/vue')['Dialog'] 15 | ArkDialogBackdrop: typeof import('@ark-ui/vue')['DialogBackdrop'] 16 | ArkDialogCloseTrigger: typeof import('@ark-ui/vue')['DialogCloseTrigger'] 17 | ArkDialogContainer: typeof import('@ark-ui/vue')['DialogContainer'] 18 | ArkDialogContent: typeof import('@ark-ui/vue')['DialogContent'] 19 | ArkDialogDescription: typeof import('@ark-ui/vue')['DialogDescription'] 20 | ArkDialogTitle: typeof import('@ark-ui/vue')['DialogTitle'] 21 | DarkModeToggle: typeof import('./src/components/DarkModeToggle.vue')['default'] 22 | EnsureAuthenticated: typeof import('./src/components/convex/EnsureAuthenticated.vue')['default'] 23 | PaginatedQuery: typeof import('./src/components/convex/PaginatedQuery.vue')['default'] 24 | PaginatedQueryInner: typeof import('./src/components/convex/PaginatedQueryInner.vue')['default'] 25 | Query: typeof import('./src/components/convex/Query.vue')['default'] 26 | QueryInner: typeof import('./src/components/convex/QueryInner.vue')['default'] 27 | RouterLink: typeof import('vue-router')['RouterLink'] 28 | RouterView: typeof import('vue-router')['RouterView'] 29 | ServiceWorkerPrompt: typeof import('./src/components/ServiceWorkerPrompt.vue')['default'] 30 | Todo: typeof import('./src/components/Todo.vue')['default'] 31 | TodoListPaginated: typeof import('./src/components/TodoListPaginated.vue')['default'] 32 | UiButton: typeof import('./src/components/ui/buttons/UiButton.vue')['default'] 33 | UiButtonBase: typeof import('./src/components/ui/buttons/UiButtonBase.vue')['default'] 34 | UiCheckbox: typeof import('./src/components/ui/inputs/UiCheckbox.vue')['default'] 35 | UiDrawer: typeof import('./src/components/ui/drawer/UiDrawer.vue')['default'] 36 | UiDrawerContent: typeof import('./src/components/ui/drawer/UiDrawerContent.vue')['default'] 37 | UiDrawerHeader: typeof import('./src/components/ui/drawer/UiDrawerHeader.vue')['default'] 38 | UiFormControl: typeof import('./src/components/ui/forms/UiFormControl.vue')['default'] 39 | UiFormError: typeof import('./src/components/ui/forms/UiFormError.vue')['default'] 40 | UiFormLabel: typeof import('./src/components/ui/forms/UiFormLabel.vue')['default'] 41 | UiGhostButton: typeof import('./src/components/ui/buttons/UiGhostButton.vue')['default'] 42 | UiIcon: typeof import('./src/components/ui/UiIcon.vue')['default'] 43 | UiIconButton: typeof import('./src/components/ui/buttons/UiIconButton.vue')['default'] 44 | UiLinkButton: typeof import('./src/components/ui/buttons/UiLinkButton.vue')['default'] 45 | UiModal: typeof import('./src/components/ui/modal/UiModal.vue')['default'] 46 | UiModalContent: typeof import('./src/components/ui/modal/UiModalContent.vue')['default'] 47 | UiModalHeader: typeof import('./src/components/ui/modal/UiModalHeader.vue')['default'] 48 | UiSimpleDrawer: typeof import('./src/components/ui/drawer/UiSimpleDrawer.vue')['default'] 49 | UiSimpleModal: typeof import('./src/components/ui/modal/UiSimpleModal.vue')['default'] 50 | UiSpinner: typeof import('./src/components/ui/UiSpinner.vue')['default'] 51 | UiSwitch: typeof import('./src/components/ui/inputs/UiSwitch.vue')['default'] 52 | UiTextInput: typeof import('./src/components/ui/inputs/UiTextInput.vue')['default'] 53 | VFocusOn: typeof import('./src/directives/vFocusOn.ts')['default'] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.1.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as todos from "../todos"; 18 | 19 | /** 20 | * A utility for referencing Convex functions in your app's API. 21 | * 22 | * Usage: 23 | * ```js 24 | * const myFunctionReference = api.myModule.myFunction; 25 | * ``` 26 | */ 27 | declare const fullApi: ApiFromModules<{ 28 | todos: typeof todos; 29 | }>; 30 | export declare const api: FilterApi< 31 | typeof fullApi, 32 | FunctionReference 33 | >; 34 | export declare const internal: FilterApi< 35 | typeof fullApi, 36 | FunctionReference 37 | >; 38 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.1.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.1.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { DataModelFromSchemaDefinition } from "convex/server"; 13 | import type { DocumentByName, TableNamesInDataModel } from "convex/server"; 14 | import type { GenericId } from "convex/values"; 15 | import schema from "../schema"; 16 | 17 | /** 18 | * The names of all of your Convex tables. 19 | */ 20 | export type TableNames = TableNamesInDataModel; 21 | 22 | /** 23 | * The type of a document stored in Convex. 24 | * 25 | * @typeParam TableName - A string literal type of the table name (like "users"). 26 | */ 27 | export type Doc = DocumentByName< 28 | DataModel, 29 | TableName 30 | >; 31 | 32 | /** 33 | * An identifier for a document in Convex. 34 | * 35 | * Convex documents are uniquely identified by their `Id`, which is accessible 36 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 37 | * 38 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 39 | * 40 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 41 | * strings when type checking. 42 | * 43 | * @typeParam TableName - A string literal type of the table name (like "users"). 44 | */ 45 | export type Id = GenericId; 46 | 47 | /** 48 | * A type describing your Convex data model. 49 | * 50 | * This type includes information about what tables you have, the type of 51 | * documents stored in those tables, and the indexes defined on them. 52 | * 53 | * This type is used to parameterize methods like `queryGeneric` and 54 | * `mutationGeneric` to make them type-safe. 55 | */ 56 | export type DataModel = DataModelFromSchemaDefinition; 57 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.1.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | GenericActionCtx, 18 | GenericMutationCtx, 19 | GenericQueryCtx, 20 | GenericDatabaseReader, 21 | GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.1.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /convex/auth.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: process.env.AUTH0_DOMAIN, 5 | applicationID: process.env.AUTH0_APPLICATIONID 6 | } 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from 'convex/server'; 2 | import { v } from 'convex/values'; 3 | 4 | export default defineSchema({ 5 | todos: defineTable({ 6 | text: v.string(), 7 | completed: v.boolean(), 8 | userId: v.string() 9 | }).index('by_userId', ['userId']) 10 | }); 11 | -------------------------------------------------------------------------------- /convex/todos.ts: -------------------------------------------------------------------------------- 1 | import { paginationOptsValidator } from 'convex/server'; 2 | import { query, mutation } from './_generated/server'; 3 | import { v } from 'convex/values'; 4 | 5 | export const paginatedList = query({ 6 | args: { paginationOpts: paginationOptsValidator }, 7 | handler: async (ctx, args) => { 8 | const identity = await ctx.auth.getUserIdentity(); 9 | if (!identity) { 10 | throw new Error('Unauthorized'); 11 | } 12 | 13 | return ctx.db 14 | .query('todos') 15 | .withIndex('by_userId', q => q.eq('userId', identity!.subject!)) 16 | .order('desc') 17 | .paginate(args.paginationOpts); 18 | } 19 | }); 20 | 21 | export const remove = mutation({ 22 | args: { id: v.id('todos') }, 23 | handler: async (ctx, { id }) => { 24 | await ctx.db.delete(id); 25 | } 26 | }); 27 | 28 | export const add = mutation({ 29 | args: { text: v.string() }, 30 | handler: async (ctx, { text }) => { 31 | const identity = await ctx.auth.getUserIdentity(); 32 | if (!identity) { 33 | throw new Error('Unauthorized'); 34 | } 35 | await ctx.db.insert('todos', { text, completed: false, userId: identity.subject }); 36 | } 37 | }); 38 | 39 | export const setCompleted = mutation({ 40 | args: { completed: v.boolean(), id: v.id('todos') }, 41 | handler: async (ctx, { id, completed }) => { 42 | await ctx.db.patch(id, { completed }); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "strict": true, 9 | 10 | /* These compiler options are required by Convex */ 11 | "target": "ESNext", 12 | "lib": ["ES2021", "dom"], 13 | "forceConsistentCasingInFileNames": true, 14 | "allowSyntheticDefaultImports": true, 15 | "module": "ESNext", 16 | "moduleResolution": "Node", 17 | "isolatedModules": true, 18 | "noEmit": true 19 | }, 20 | "include": ["./**/*"], 21 | "exclude": ["./_generated"] 22 | } 23 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackathon", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "npm-run-all type-check build-only", 7 | "dev": "npm-run-all --parallel dev:server dev:client", 8 | "dev:server": "convex dev", 9 | "dev:client": "vite", 10 | "preview": "vite preview", 11 | "build-only": "vite build", 12 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", 13 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 14 | "format": "prettier --write src/" 15 | }, 16 | "dependencies": { 17 | "@ark-ui/vue": "^0.7.0", 18 | "@auth0/auth0-vue": "^2.3.1", 19 | "@formkit/auto-animate": "^0.7.0", 20 | "@iconify/json": "^2.2.110", 21 | "@iconify/vue": "^4.1.1", 22 | "@vee-validate/zod": "^4.11.3", 23 | "@vueuse/core": "^10.4.1", 24 | "convex": "^1.1.1", 25 | "lodash-es": "^4.17.21", 26 | "mitt": "^3.0.1", 27 | "open-props": "^1.5.15", 28 | "vee-validate": "^4.11.3", 29 | "vue": "^3.3.4", 30 | "vue-router": "^4.2.4", 31 | "zod": "^3.22.2" 32 | }, 33 | "devDependencies": { 34 | "@rushstack/eslint-patch": "^1.3.2", 35 | "@tsconfig/node18": "^18.2.0", 36 | "@types/css-tree": "^2.3.1", 37 | "@types/lodash-es": "^4.17.9", 38 | "@types/node": "^18.17.5", 39 | "@unocss/postcss": "^0.53.5", 40 | "@vitejs/plugin-vue": "^4.3.1", 41 | "@vue/eslint-config-prettier": "^8.0.0", 42 | "@vue/eslint-config-typescript": "^11.0.3", 43 | "@vue/tsconfig": "^0.4.0", 44 | "autoprefixer": "^10.4.15", 45 | "css-tree": "^2.3.1", 46 | "cssnano": "^6.0.1", 47 | "eslint": "^8.46.0", 48 | "eslint-plugin-vue": "^9.16.1", 49 | "npm-run-all": "^4.1.5", 50 | "postcss-custom-media": "^10.0.0", 51 | "postcss-html": "^1.5.0", 52 | "postcss-nesting": "^12.0.0", 53 | "postcss-scrollbar": "^0.5.1", 54 | "postcss-syntax": "^0.36.2", 55 | "prettier": "^3.0.0", 56 | "stylelint": "^15.9.0", 57 | "stylelint-config-clean-order": "^5.0.1", 58 | "stylelint-config-html": "^1.1.0", 59 | "stylelint-config-recommended-vue": "^1.4.0", 60 | "typescript": "~5.1.6", 61 | "unocss": "^0.53.5", 62 | "unplugin-auto-import": "^0.16.6", 63 | "unplugin-vue-components": "^0.25.2", 64 | "unplugin-vue-router": "^0.6.4", 65 | "vite": "^4.4.9", 66 | "vite-plugin-pwa": "^0.16.4", 67 | "vue-tsc": "^1.8.8" 68 | }, 69 | "resolutions": { 70 | "array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@latest", 71 | "arraybuffer.prototype.slice": "npm:@nolyfill/arraybuffer.prototype.slice@latest", 72 | "available-typed-arrays": "npm:@nolyfill/available-typed-arrays@latest", 73 | "define-properties": "npm:@nolyfill/define-properties@latest", 74 | "es-set-tostringtag": "npm:@nolyfill/es-set-tostringtag@latest", 75 | "function-bind": "npm:@nolyfill/function-bind@latest", 76 | "function.prototype.name": "npm:@nolyfill/function.prototype.name@latest", 77 | "get-symbol-description": "npm:@nolyfill/get-symbol-description@latest", 78 | "globalthis": "npm:@nolyfill/globalthis@latest", 79 | "gopd": "npm:@nolyfill/gopd@latest", 80 | "has": "npm:@nolyfill/has@latest", 81 | "has-property-descriptors": "npm:@nolyfill/has-property-descriptors@latest", 82 | "has-proto": "npm:@nolyfill/has-proto@latest", 83 | "has-symbols": "npm:@nolyfill/has-symbols@latest", 84 | "has-tostringtag": "npm:@nolyfill/has-tostringtag@latest", 85 | "internal-slot": "npm:@nolyfill/internal-slot@latest", 86 | "is-array-buffer": "npm:@nolyfill/is-array-buffer@latest", 87 | "is-date-object": "npm:@nolyfill/is-date-object@latest", 88 | "is-regex": "npm:@nolyfill/is-regex@latest", 89 | "is-shared-array-buffer": "npm:@nolyfill/is-shared-array-buffer@latest", 90 | "is-string": "npm:@nolyfill/is-string@latest", 91 | "is-symbol": "npm:@nolyfill/is-symbol@latest", 92 | "is-weakref": "npm:@nolyfill/is-weakref@latest", 93 | "object-keys": "npm:@nolyfill/object-keys@latest", 94 | "object.assign": "npm:@nolyfill/object.assign@latest", 95 | "regexp.prototype.flags": "npm:@nolyfill/regexp.prototype.flags@latest", 96 | "safe-array-concat": "npm:@nolyfill/safe-array-concat@latest", 97 | "safe-regex-test": "npm:@nolyfill/safe-regex-test@latest", 98 | "side-channel": "npm:@nolyfill/side-channel@latest", 99 | "string.prototype.matchall": "npm:@nolyfill/string.prototype.matchall@latest", 100 | "string.prototype.padend": "npm:@nolyfill/string.prototype.padend@latest", 101 | "string.prototype.trim": "npm:@nolyfill/string.prototype.trim@latest", 102 | "string.prototype.trimend": "npm:@nolyfill/string.prototype.trimend@latest", 103 | "string.prototype.trimstart": "npm:@nolyfill/string.prototype.trimstart@latest", 104 | "typed-array-buffer": "npm:@nolyfill/typed-array-buffer@latest", 105 | "typed-array-byte-length": "npm:@nolyfill/typed-array-byte-length@latest", 106 | "typed-array-byte-offset": "npm:@nolyfill/typed-array-byte-offset@latest", 107 | "typed-array-length": "npm:@nolyfill/typed-array-length@latest", 108 | "unbox-primitive": "npm:@nolyfill/unbox-primitive@latest", 109 | "which-boxed-primitive": "npm:@nolyfill/which-boxed-primitive@latest", 110 | "which-typed-array": "npm:@nolyfill/which-typed-array@latest" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | cssnano: {}, 5 | 'postcss-scrollbar': {}, 6 | 'postcss-nesting': { noIsPseudoSelector: false }, 7 | 'postcss-custom-media': { preserve: false }, 8 | '@unocss/postcss': {} 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Darialyphia/convex-vue-vite-template/ffb816d1b36cb5249f82ac077340a2c123992e0c/public/favicon.ico -------------------------------------------------------------------------------- /public/icon/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Darialyphia/convex-vue-vite-template/ffb816d1b36cb5249f82ac077340a2c123992e0c/public/icon/icon-192x192.png -------------------------------------------------------------------------------- /public/icon/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Darialyphia/convex-vue-vite-template/ffb816d1b36cb5249f82ac077340a2c123992e0c/public/icon/icon-512x512.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 79 | 80 | 103 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | export { api } from '../convex/_generated/api'; 2 | -------------------------------------------------------------------------------- /src/components/AddTodoForm.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 42 | -------------------------------------------------------------------------------- /src/components/DarkModeToggle.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /src/components/ServiceWorkerPrompt.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 39 | 78 | -------------------------------------------------------------------------------- /src/components/Todo.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | 37 | 48 | -------------------------------------------------------------------------------- /src/components/TodoListPaginated.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/components/convex/EnsureAuthenticated.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /src/components/convex/PaginatedQuery.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 52 | -------------------------------------------------------------------------------- /src/components/convex/PaginatedQueryInner.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | -------------------------------------------------------------------------------- /src/components/convex/Query.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 62 | -------------------------------------------------------------------------------- /src/components/convex/QueryInner.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /src/components/ui/UiIcon.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/ui/UiSpinner.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | 62 | -------------------------------------------------------------------------------- /src/components/ui/buttons/UiButton.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 44 | 45 | 60 | -------------------------------------------------------------------------------- /src/components/ui/buttons/UiButtonBase.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 81 | 82 | 139 | -------------------------------------------------------------------------------- /src/components/ui/buttons/UiGhostButton.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 41 | 42 | 68 | -------------------------------------------------------------------------------- /src/components/ui/buttons/UiIconButton.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | 27 | 35 | s 36 | -------------------------------------------------------------------------------- /src/components/ui/buttons/UiLinkButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 32 | -------------------------------------------------------------------------------- /src/components/ui/drawer/UiDrawer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | 34 | 98 | -------------------------------------------------------------------------------- /src/components/ui/drawer/UiDrawerContent.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/ui/drawer/UiDrawerHeader.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | 33 | 61 | -------------------------------------------------------------------------------- /src/components/ui/drawer/UiSimpleDrawer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /src/components/ui/forms/UiFormControl.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /src/components/ui/forms/UiFormError.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | 39 | 63 | -------------------------------------------------------------------------------- /src/components/ui/forms/UiFormLabel.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /src/components/ui/inputs/UiCheckbox.vue: -------------------------------------------------------------------------------- 1 | 38 | 53 | 54 | 83 | -------------------------------------------------------------------------------- /src/components/ui/inputs/UiSwitch.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 33 | 34 | 87 | -------------------------------------------------------------------------------- /src/components/ui/inputs/UiTextInput.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 95 | 96 | 190 | -------------------------------------------------------------------------------- /src/components/ui/modal/UiModal.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | 40 | 104 | -------------------------------------------------------------------------------- /src/components/ui/modal/UiModalContent.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/ui/modal/UiModalHeader.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | 31 | 59 | -------------------------------------------------------------------------------- /src/components/ui/modal/UiSimpleModal.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /src/composables/convex/useAction.ts: -------------------------------------------------------------------------------- 1 | import { makeFunctionReference, type FunctionReference } from 'convex/server'; 2 | 3 | export type ActionReference = FunctionReference<'action'>; 4 | export function useAction(action: Action) { 5 | const convex = useConvex(); 6 | 7 | const actionReference = 8 | typeof action === 'string' 9 | ? makeFunctionReference<'action', any, any>(action) 10 | : action; 11 | 12 | const isLoading = ref(false); 13 | 14 | return { 15 | isLoading, 16 | execute: async (args?: Action['_args']): Promise => { 17 | try { 18 | isLoading.value = true; 19 | return await convex.action(actionReference, args); 20 | } finally { 21 | isLoading.value = false; 22 | } 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/composables/convex/useConvex.ts: -------------------------------------------------------------------------------- 1 | import { CONVEX_INJECTION_KEY, CONVEX_AUTH_INJECTION_KEY } from '@/plugins/convex'; 2 | 3 | export const useConvex = () => { 4 | return useSafeInject(CONVEX_INJECTION_KEY); 5 | }; 6 | 7 | export const useConvexAuth = () => { 8 | return useSafeInject(CONVEX_AUTH_INJECTION_KEY); 9 | }; 10 | -------------------------------------------------------------------------------- /src/composables/convex/useMutation.ts: -------------------------------------------------------------------------------- 1 | import type { OptimisticUpdate } from 'convex/browser'; 2 | import { makeFunctionReference, type FunctionReference } from 'convex/server'; 3 | 4 | export type MutationReference = FunctionReference<'mutation'>; 5 | export function useMutation( 6 | mutation: Mutation, 7 | { optimisticUpdate }: { optimisticUpdate?: OptimisticUpdate } = {} 8 | ) { 9 | const convex = useConvex(); 10 | 11 | const mutationReference = 12 | typeof mutation === 'string' 13 | ? makeFunctionReference<'mutation', any, any>(mutation) 14 | : mutation; 15 | 16 | const isLoading = ref(false); 17 | 18 | return { 19 | isLoading, 20 | mutate: async (args?: Mutation['_args']): Promise => { 21 | try { 22 | isLoading.value = true; 23 | return await convex.mutation(mutationReference as Mutation, args, { 24 | optimisticUpdate 25 | }); 26 | } finally { 27 | isLoading.value = false; 28 | } 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/composables/convex/usePaginatedQuery.ts: -------------------------------------------------------------------------------- 1 | import { type ComputedRef } from 'vue'; 2 | import type { BetterOmit, Expand } from '@/utils/types'; 3 | import { 4 | type FunctionArgs, 5 | type FunctionReference, 6 | type PaginationOptions, 7 | paginationOptsValidator, 8 | getFunctionName, 9 | type FunctionReturnType, 10 | type PaginationResult 11 | } from 'convex/server'; 12 | import { convexToJson, type Infer, type Value } from 'convex/values'; 13 | import { useQueries } from './useQueries'; 14 | import type { MaybeRefOrGetter } from '@vueuse/core'; 15 | 16 | export type PaginatedQueryItem = 17 | FunctionReturnType['page'][number]; 18 | 19 | export type UsePaginatedQueryResult = { 20 | results: ComputedRef; 21 | loadMore: (numItems: number) => void; 22 | } & ( 23 | | { 24 | status: ComputedRef<'LoadingFirstPage'>; 25 | isLoading: ComputedRef; 26 | } 27 | | { 28 | status: ComputedRef<'CanLoadMore'>; 29 | isLoading: ComputedRef; 30 | } 31 | | { 32 | status: ComputedRef<'LoadingMore'>; 33 | isLoading: ComputedRef; 34 | } 35 | | { 36 | status: ComputedRef<'Exhausted'>; 37 | isLoading: ComputedRef; 38 | } 39 | ); 40 | 41 | export type PaginatedQueryReference = FunctionReference< 42 | 'query', 43 | 'public', 44 | { paginationOpts: PaginationOptions }, 45 | PaginationResult 46 | >; 47 | 48 | export type PaginatedQueryArgs = Expand< 49 | BetterOmit, 'paginationOpts'> 50 | >; 51 | 52 | export type UsePaginatedQueryReturnType = 53 | UsePaginatedQueryResult>; 54 | 55 | let paginationId = 0; 56 | 57 | function nextPaginationId(): number { 58 | paginationId++; 59 | return paginationId; 60 | } 61 | 62 | export function usePaginatedQuery( 63 | query: Query, 64 | args: MaybeRefOrGetter>, 65 | options: { initialNumItems: number } 66 | ): UsePaginatedQueryReturnType { 67 | if (typeof options?.initialNumItems !== 'number' || options.initialNumItems < 0) { 68 | throw new Error( 69 | `\`options.initialNumItems\` must be a positive number. Received \`${options?.initialNumItems}\`.` 70 | ); 71 | } 72 | 73 | const createInitialState = () => { 74 | const id = nextPaginationId(); 75 | return { 76 | query, 77 | id, 78 | maxQueryIndex: 0, 79 | queries: { 80 | 0: { 81 | query, 82 | args: { 83 | ...args, 84 | paginationOpts: { 85 | numItems: options.initialNumItems, 86 | cursor: null, 87 | id 88 | } 89 | } 90 | } 91 | } 92 | }; 93 | }; 94 | const state = ref<{ 95 | query: FunctionReference<'query'>; 96 | id: number; 97 | maxQueryIndex: number; 98 | queries: Record< 99 | number, 100 | { 101 | query: FunctionReference<'query'>; 102 | // Use the validator type as a test that it matches the args 103 | // we generate. 104 | args: { paginationOpts: Infer }; 105 | } 106 | >; 107 | }>(createInitialState()); 108 | 109 | const resultsObject = useQueries(computed(() => state.value.queries)); 110 | 111 | const hasRecoverableError = computed(() => { 112 | let hasError = false; 113 | for (let i = 0; i <= state.value.maxQueryIndex; i++) { 114 | const currResult = resultsObject.value[i]; 115 | if (currResult === undefined) { 116 | break; 117 | } 118 | 119 | if (currResult instanceof Error) { 120 | if ( 121 | currResult.message.includes('InvalidCursor') || 122 | currResult.message.includes('ArrayTooLong') || 123 | currResult.message.includes('TooManyReads') || 124 | currResult.message.includes('TooManyDocumentsRead') || 125 | currResult.message.includes('ReadsTooLarge') 126 | ) { 127 | // `usePaginatedQueryGeneric` handles a few types of query errors: 128 | 129 | // - InvalidCursor: If the cursor is invalid, probably the paginated 130 | // database query was data-dependent and changed underneath us. The 131 | // cursor in the params or journal no longer matches the current 132 | // database query. 133 | // - ArrayTooLong, TooManyReads, TooManyDocumentsRead, ReadsTooLarge: 134 | // Likely so many elements were added to a single page they hit our limit. 135 | 136 | // In all cases, we want to restart pagination to throw away all our 137 | // existing cursors. 138 | console.warn( 139 | 'usePaginatedQuery hit error, resetting pagination state: ' + 140 | currResult.message 141 | ); 142 | hasError = true; 143 | } 144 | } 145 | } 146 | 147 | return hasError; 148 | }); 149 | 150 | const unwrappedArgs = computed(() => toValue(args)); 151 | watch( 152 | [ 153 | () => hasRecoverableError.value, 154 | () => getFunctionName(query) !== getFunctionName(state.value.query), 155 | unwrappedArgs 156 | ], 157 | ([hasRecoverableError, queryHasChanged, newArgs], [, , oldArgs]) => { 158 | const argsHaveChanged = JSON.stringify(oldArgs) !== JSON.stringify(newArgs); 159 | 160 | if (hasRecoverableError || queryHasChanged || argsHaveChanged) { 161 | state.value = createInitialState(); 162 | } 163 | } 164 | ); 165 | 166 | const results = computed<{ 167 | allPages: Value[]; 168 | lastPage: undefined | PaginationResult; 169 | }>(() => { 170 | let lastPage = undefined; 171 | 172 | const allPages: Value[] = []; 173 | 174 | if (hasRecoverableError.value) return { allPages, lastPage }; 175 | 176 | for (let i = 0; i <= state.value.maxQueryIndex; i++) { 177 | lastPage = resultsObject.value[i]; 178 | if (lastPage === undefined) { 179 | break; 180 | } 181 | if ( 182 | lastPage instanceof Error && 183 | !lastPage.message.includes('InvalidCursor') && 184 | !lastPage.message.includes('ArrayTooLong') && 185 | !lastPage.message.includes('TooManyReads') && 186 | !lastPage.message.includes('TooManyDocumentsRead') && 187 | !lastPage.message.includes('ReadsTooLarge') 188 | ) { 189 | console.log('throwing'); 190 | throw lastPage; 191 | } 192 | allPages.push(...lastPage.page); 193 | } 194 | return { allPages, lastPage }; 195 | }); 196 | 197 | const statusObject = computed(() => { 198 | const maybeLastResult = results.value.lastPage; 199 | if (maybeLastResult === undefined) { 200 | if (state.value.maxQueryIndex === 0) { 201 | return { 202 | status: 'LoadingFirstPage', 203 | isLoading: true 204 | } as const; 205 | } else { 206 | return { 207 | status: 'LoadingMore', 208 | isLoading: true 209 | } as const; 210 | } 211 | } 212 | if (maybeLastResult.isDone) { 213 | return { 214 | status: 'Exhausted', 215 | isLoading: false 216 | } as const; 217 | } 218 | 219 | return { 220 | status: 'CanLoadMore', 221 | isLoading: false 222 | }; 223 | }); 224 | 225 | const isLoadingMore = ref(false); 226 | watchEffect(() => { 227 | const { lastPage } = results.value; 228 | if (lastPage === undefined) return; 229 | isLoadingMore.value = false; 230 | }); 231 | 232 | const loadMore = (numItems: number) => { 233 | console.log(isLoadingMore.value); 234 | if (isLoadingMore.value) return; 235 | 236 | const { lastPage } = results.value; 237 | if (lastPage === undefined) return; 238 | if (lastPage.isDone) return; 239 | 240 | isLoadingMore.value = true; 241 | 242 | state.value.maxQueryIndex++; 243 | state.value.queries[state.value.maxQueryIndex] = { 244 | query: state.value.query, 245 | args: { 246 | ...unwrappedArgs.value, 247 | paginationOpts: { 248 | numItems, 249 | cursor: lastPage.continueCursor, 250 | id: state.value.id 251 | } 252 | } 253 | }; 254 | }; 255 | 256 | return { 257 | results: computed(() => results.value.allPages), 258 | status: computed(() => statusObject.value.status), 259 | isLoading: computed(() => statusObject.value.isLoading), 260 | loadMore 261 | } as UsePaginatedQueryReturnType; 262 | } 263 | -------------------------------------------------------------------------------- /src/composables/convex/useQueries.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue'; 2 | import type { Value } from 'convex/values'; 3 | import type { QueryReference } from './useQuery'; 4 | import type { QueryJournal } from 'convex/browser'; 5 | 6 | import type { Watch } from '@/plugins/convex'; 7 | import { QueriesObserver } from '@/utils/convex/QueriesObserver'; 8 | import type { FunctionReference } from 'convex/server'; 9 | 10 | export function useQueries( 11 | queries: Ref 12 | ): Record { 13 | const convex = useConvex(); 14 | 15 | const createWatch = ( 16 | query: QueryReference, 17 | args: Record, 18 | journal?: QueryJournal 19 | ) => { 20 | return convex.watchQuery(query, args, { journal }); 21 | }; 22 | 23 | return useQueriesHelper(queries, createWatch); 24 | } 25 | 26 | export type CreateWatch = ( 27 | query: QueryReference, 28 | args: Record, 29 | journal?: QueryJournal 30 | ) => Watch; 31 | 32 | /** 33 | * Internal version of `useQueriesGeneric` that is exported for testing. 34 | */ 35 | export function useQueriesHelper( 36 | queries: Ref, 37 | createWatch: CreateWatch 38 | ): Record { 39 | const observer = new QueriesObserver(createWatch); 40 | 41 | watchEffect(() => { 42 | if (observer.createWatch !== createWatch) { 43 | observer.setCreateWatch(createWatch); 44 | } 45 | }); 46 | 47 | watchEffect(() => { 48 | observer.setQueries(queries.value); 49 | }); 50 | 51 | onUnmounted(() => { 52 | observer.destroy(); 53 | }); 54 | 55 | const result = ref(observer.getCurrentQueries()); 56 | 57 | observer.subscribe(() => { 58 | result.value = observer.getCurrentQueries(); 59 | }); 60 | 61 | return result; 62 | } 63 | 64 | export type RequestForQueries = Record< 65 | string, 66 | { 67 | query: FunctionReference<'query'>; 68 | args: Record; 69 | } 70 | >; 71 | -------------------------------------------------------------------------------- /src/composables/convex/useQuery.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from '@vueuse/core'; 2 | import { 3 | makeFunctionReference, 4 | type FunctionReference, 5 | type OptionalRestArgs 6 | } from 'convex/server'; 7 | 8 | export type QueryReference = FunctionReference<'query'>; 9 | 10 | export const useQuery = ( 11 | query: Query, 12 | args: MaybeRefOrGetter> 13 | ): Ref => { 14 | const convex = useConvex(); 15 | 16 | const queryReference = 17 | typeof query === 'string' ? makeFunctionReference<'query', any, any>(query) : query; 18 | 19 | const data = ref(); 20 | 21 | watchEffect(onCleanup => { 22 | const { onUpdate, localQueryResult } = convex.watchQuery( 23 | queryReference, 24 | ...toValue(args) 25 | ); 26 | data.value = localQueryResult(); 27 | 28 | const unsub = onUpdate(() => { 29 | const newVal = localQueryResult(); 30 | 31 | data.value = newVal; 32 | }); 33 | 34 | onCleanup(unsub); 35 | }); 36 | 37 | return data; 38 | }; 39 | -------------------------------------------------------------------------------- /src/composables/convex/useSuspenseQuery.ts: -------------------------------------------------------------------------------- 1 | import { makeFunctionReference, type OptionalRestArgs } from 'convex/server'; 2 | import type { QueryReference } from './useQuery'; 3 | import type { MaybeRefOrGetter } from '@vueuse/core'; 4 | 5 | export const useSuspenseQuery = ( 6 | query: Query, 7 | args: MaybeRefOrGetter> 8 | ): Promise> => { 9 | const convex = useConvex(); 10 | 11 | const queryReference = 12 | typeof query === 'string' ? makeFunctionReference<'query', any, any>(query) : query; 13 | 14 | return new Promise>((res, rej) => { 15 | const data = ref(); 16 | 17 | watchEffect(onCleanup => { 18 | const { onUpdate, localQueryResult } = convex.watchQuery( 19 | queryReference, 20 | ...toValue(args) 21 | ); 22 | const initialValue = localQueryResult(); 23 | data.value = initialValue; 24 | 25 | const unsub = onUpdate(() => { 26 | try { 27 | const newVal = localQueryResult(); 28 | data.value = newVal; 29 | res(data); 30 | } catch (err) { 31 | rej(err); 32 | } 33 | }); 34 | 35 | if (initialValue) res(data); 36 | 37 | onCleanup(unsub); 38 | }); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/composables/useFocusOn.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | 3 | export const focusEmitter = mitt<{ focus: string }>(); 4 | 5 | export const useFocusOn = () => { 6 | return (target: string) => focusEmitter.emit('focus', target); 7 | }; 8 | -------------------------------------------------------------------------------- /src/composables/useSafeInject.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue'; 2 | 3 | export const useSafeInject = (injectionKey: InjectionKey): T => { 4 | const context = inject(injectionKey); 5 | 6 | if (context === undefined) { 7 | throw new Error( 8 | `Your are trying to use ${injectionKey.toString()} outside of its provider.` 9 | ); 10 | } 11 | 12 | return context; 13 | }; 14 | -------------------------------------------------------------------------------- /src/composables/useStyles.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter, StyleValue } from 'vue'; 2 | import type { AnyObject } from '@/utils/types'; 3 | import { kebabCase } from 'lodash-es'; 4 | 5 | export type ThemeProps = { 6 | theme?: { [key in TKeys]?: string }; 7 | }; 8 | 9 | type WithThemeProps = TObj & 10 | ThemeProps; 11 | 12 | const rawValueRE = /^\[([^\]]+)]$/; // matches content in brackets, ie: [#123456] 13 | 14 | const transformValue = (val: string) => { 15 | return rawValueRE.test(val) 16 | ? val.replace(/^\[/, '').replace(/]$/, '') 17 | : `var(--${val})`; 18 | }; 19 | 20 | const appliedDefaults = new Set(); 21 | 22 | export const useStyles = ( 23 | { 24 | config, 25 | prefix = '' 26 | }: { 27 | config: Required>; 28 | prefix?: string; 29 | }, 30 | getTheme: MaybeRefOrGetter['theme']> 31 | ) => { 32 | if (!appliedDefaults.has(prefix)) { 33 | appliedDefaults.add(prefix); 34 | 35 | Object.entries(config).map(([key, defaultValue]) => { 36 | const fallback = transformValue(defaultValue as string); 37 | const name = `--${prefix}${prefix ? '-' : ''}${kebabCase(key)}`; 38 | document.documentElement.style.setProperty(name, fallback); 39 | }); 40 | } 41 | 42 | return computed(() => { 43 | const theme = toValue(getTheme); 44 | 45 | return Object.fromEntries( 46 | Object.keys(config) 47 | .map(key => { 48 | const name = `--${prefix}${prefix ? '-' : ''}${kebabCase(key)}`; 49 | const value = theme?.[key as T]; 50 | 51 | return [name, isString(value) ? transformValue(value) : undefined]; 52 | }) 53 | .filter(([, v]) => isDefined(v)) 54 | ) as Record; 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /src/directives/vFocusOn.ts: -------------------------------------------------------------------------------- 1 | import { focusEmitter } from '@/composables/useFocusOn'; 2 | import type { Directive } from 'vue'; 3 | 4 | export const vFocusOn: Directive = { 5 | mounted(el, binding) { 6 | focusEmitter.on('focus', target => { 7 | if (target === binding.value) { 8 | el.focus(); 9 | nextTick(() => { 10 | // can happen when used on a component that doesn't have its "focusable" as the root element 11 | if (document.activeElement != el) { 12 | getFocusableChildren(el).at(0)?.focus(); 13 | } 14 | }); 15 | } 16 | }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import '@/styles/global.css'; 2 | import { createApp } from 'vue'; 3 | import { createRouter, createWebHistory } from 'vue-router/auto'; 4 | import { createAuth0 } from '@auth0/auth0-vue'; 5 | import { createConvex } from './plugins/convex'; 6 | import { autoAnimatePlugin } from '@formkit/auto-animate/vue'; 7 | 8 | declare module 'vue-router/auto' { 9 | interface RouteMeta { 10 | needsAuth?: boolean; 11 | } 12 | } 13 | 14 | declare module 'vue-router' { 15 | interface RouteMeta { 16 | needsAuth?: boolean; 17 | } 18 | } 19 | 20 | import App from './App.vue'; 21 | const app = createApp(App); 22 | 23 | app.use( 24 | createRouter({ 25 | history: createWebHistory() 26 | }) 27 | ); 28 | app.use( 29 | createAuth0({ 30 | domain: import.meta.env.VITE_AUTH0_DOMAIN, 31 | clientId: import.meta.env.VITE_AUTH0_CLIENTID, 32 | authorizationParams: { 33 | redirect_uri: window.location.origin 34 | } 35 | }) 36 | ); 37 | app.use( 38 | createConvex(import.meta.env.VITE_CONVEX_URL, { 39 | auth0: { 40 | installNavigationGuard: true, 41 | redirectTo: () => '/', 42 | needsAuth: to => !!to.meta.needsAuth 43 | } 44 | }) 45 | ); 46 | app.use(autoAnimatePlugin); 47 | app.mount('#app'); 48 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | -------------------------------------------------------------------------------- /src/pages/profile/[id].vue: -------------------------------------------------------------------------------- 1 | 13 | 28 | -------------------------------------------------------------------------------- /src/plugins/convex.ts: -------------------------------------------------------------------------------- 1 | import type { App, InjectionKey, Plugin, Ref } from 'vue'; 2 | import type { Router } from 'vue-router/auto'; 3 | import type { RouteLocationNormalized } from 'vue-router/auto'; 4 | import type { RouteLocationRaw } from 'vue-router/auto'; 5 | import { 6 | BaseConvexClient, 7 | type QueryToken, 8 | type ClientOptions, 9 | type OptimisticUpdate, 10 | type QueryJournal 11 | } from 'convex/browser'; 12 | import type { AuthTokenFetcher } from 'convex/react'; 13 | import { 14 | getFunctionName, 15 | type ArgsAndOptions, 16 | type FunctionReference, 17 | type FunctionReturnType, 18 | type UserIdentity, 19 | type FunctionArgs, 20 | type OptionalRestArgs 21 | } from 'convex/server'; 22 | import type { Value } from 'convex/values'; 23 | 24 | export type UserIdentityAttributes = Omit; 25 | 26 | export type ConnectionState = { 27 | hasInflightRequests: boolean; 28 | isWebSocketConnected: boolean; 29 | timeOfOldestInflightRequest: Date | null; 30 | }; 31 | 32 | export interface MutationOptions> { 33 | optimisticUpdate?: OptimisticUpdate; 34 | } 35 | 36 | export interface Watch { 37 | onUpdate(callback: () => void): () => void; 38 | localQueryResult(): T | undefined; 39 | localQueryLogs(): string[] | undefined; 40 | journal(): QueryJournal | undefined; 41 | } 42 | 43 | export interface WatchQueryOptions { 44 | journal?: QueryJournal; 45 | } 46 | 47 | export class ConvexVueClient { 48 | private address: string; 49 | private cachedSync?: BaseConvexClient; 50 | private listeners: Map void>>; 51 | private options: ClientOptions; 52 | private closed = false; 53 | 54 | private adminAuth?: string; 55 | private fakeUserIdentity?: UserIdentityAttributes; 56 | 57 | constructor(address: string, options?: ClientOptions) { 58 | if (typeof address !== 'string') { 59 | throw new Error( 60 | "ConvexReactClient requires a URL like 'https://happy-otter-123.convex.cloud'." 61 | ); 62 | } 63 | if (!address.includes('://')) { 64 | throw new Error('Provided address was not an absolute URL.'); 65 | } 66 | this.address = address; 67 | this.listeners = new Map(); 68 | this.options = { ...options }; 69 | } 70 | 71 | get sync() { 72 | if (this.closed) { 73 | throw new Error('ConvexReactClient has already been closed.'); 74 | } 75 | if (this.cachedSync) { 76 | return this.cachedSync; 77 | } 78 | this.cachedSync = new BaseConvexClient( 79 | this.address, 80 | updatedQueries => this.transition(updatedQueries), 81 | this.options 82 | ); 83 | if (this.adminAuth) { 84 | // @ts-ignore internal deez nuts 85 | this.cachedSync.setAdminAuth(this.adminAuth, this.fakeUserIdentity); 86 | } 87 | return this.cachedSync; 88 | } 89 | 90 | setAuth(fetchToken: AuthTokenFetcher, onChange?: (isAuthenticated: boolean) => void) { 91 | if (typeof fetchToken === 'string') { 92 | throw new Error( 93 | 'Passing a string to ConvexVueClient.setAuth is no longer supported, ' + 94 | 'please upgrade to passing in an async function to handle reauthentication.' 95 | ); 96 | } 97 | this.sync.setAuth( 98 | fetchToken, 99 | onChange ?? 100 | (() => { 101 | // Do nothing 102 | }) 103 | ); 104 | } 105 | 106 | clearAuth() { 107 | this.sync.clearAuth(); 108 | } 109 | 110 | setAdminAuth(token: string, identity?: UserIdentityAttributes) { 111 | this.adminAuth = token; 112 | this.fakeUserIdentity = identity; 113 | if (this.closed) { 114 | throw new Error('ConvexVueClient has already been closed.'); 115 | } 116 | if (this.cachedSync) { 117 | // @ts-ignore internal deez nuts 118 | this.sync.setAdminAuth(token, identity); 119 | } 120 | } 121 | 122 | watchQuery>( 123 | query: Query, 124 | ...argsAndOptions: ArgsAndOptions 125 | ): Watch> { 126 | const [args, options] = argsAndOptions; 127 | const name = getFunctionName(query); 128 | 129 | return { 130 | onUpdate: callback => { 131 | const { queryToken, unsubscribe } = this.sync.subscribe( 132 | name as string, 133 | args, 134 | options 135 | ); 136 | 137 | const currentListeners = this.listeners.get(queryToken); 138 | if (currentListeners !== undefined) { 139 | currentListeners.add(callback); 140 | } else { 141 | this.listeners.set(queryToken, new Set([callback])); 142 | } 143 | 144 | return () => { 145 | if (this.closed) { 146 | return; 147 | } 148 | 149 | const currentListeners = this.listeners.get(queryToken)!; 150 | currentListeners.delete(callback); 151 | if (currentListeners.size === 0) { 152 | this.listeners.delete(queryToken); 153 | } 154 | unsubscribe(); 155 | }; 156 | }, 157 | 158 | localQueryResult: () => { 159 | // Use the cached client because we can't have a query result if we don't 160 | // even have a client yet! 161 | if (this.cachedSync) { 162 | return this.cachedSync.localQueryResult(name, args); 163 | } 164 | return undefined; 165 | }, 166 | 167 | localQueryLogs: () => { 168 | if (this.cachedSync) { 169 | // @ts-ignore internal deez nuts 170 | return this.cachedSync.localQueryLogs(name, args); 171 | } 172 | return undefined; 173 | }, 174 | 175 | journal: () => { 176 | if (this.cachedSync) { 177 | return this.cachedSync.queryJournal(name, args); 178 | } 179 | return undefined; 180 | } 181 | }; 182 | } 183 | 184 | mutation>( 185 | mutation: Mutation, 186 | ...argsAndOptions: ArgsAndOptions>> 187 | ): Promise> { 188 | const [args, options] = argsAndOptions; 189 | const name = getFunctionName(mutation); 190 | return this.sync.mutation(name, args, options); 191 | } 192 | 193 | action>( 194 | action: Action, 195 | ...args: OptionalRestArgs 196 | ): Promise> { 197 | const name = getFunctionName(action); 198 | return this.sync.action(name, ...args); 199 | } 200 | 201 | query>( 202 | query: Query, 203 | ...args: OptionalRestArgs 204 | ): Promise> { 205 | const watch = this.watchQuery(query, ...args); 206 | const existingResult = watch.localQueryResult(); 207 | if (existingResult !== undefined) { 208 | return existingResult; 209 | } 210 | return new Promise(resolve => { 211 | const unsubscribe = watch.onUpdate(() => { 212 | unsubscribe(); 213 | resolve(watch.localQueryResult()); 214 | }); 215 | }); 216 | } 217 | 218 | connectionState(): ConnectionState { 219 | return this.sync.connectionState(); 220 | } 221 | 222 | async close(): Promise { 223 | this.closed = true; 224 | this.listeners = new Map(); 225 | if (this.cachedSync) { 226 | const sync = this.cachedSync; 227 | this.cachedSync = undefined; 228 | await sync.close(); 229 | } 230 | } 231 | 232 | private transition(updatedQueries: QueryToken[]) { 233 | for (const queryToken of updatedQueries) { 234 | const callbacks = this.listeners.get(queryToken); 235 | if (callbacks) { 236 | for (const callback of callbacks) { 237 | callback(); 238 | } 239 | } 240 | } 241 | } 242 | } 243 | 244 | export const CONVEX_INJECTION_KEY = Symbol('convex') as InjectionKey; 245 | 246 | type Auth0Options = 247 | | { 248 | installNavigationGuard: true; 249 | needsAuth: (to: RouteLocationNormalized, from?: RouteLocationNormalized) => boolean; 250 | redirectTo: ( 251 | to: RouteLocationNormalized, 252 | from?: RouteLocationNormalized 253 | ) => RouteLocationRaw; 254 | } 255 | | { 256 | installNavigationGuard?: false; 257 | needsAuth?: never; 258 | redirectTo?: never; 259 | }; 260 | 261 | type CreateConvexOptions = { 262 | auth0?: Auth0Options; 263 | }; 264 | 265 | type ConvexAuthState = { 266 | isLoading: Readonly>; 267 | isAuthenticated: Readonly>; 268 | }; 269 | 270 | export const CONVEX_AUTH_INJECTION_KEY = Symbol( 271 | 'convex' 272 | ) as InjectionKey; 273 | 274 | const installNavigationGuard = ( 275 | authState: ConvexAuthState, 276 | router: Router, 277 | { 278 | needsAuth, 279 | redirectTo 280 | }: Pick 281 | ) => { 282 | router.beforeEach(async (to, from, next) => { 283 | if (!needsAuth(to, from)) return next(); 284 | 285 | await until(authState.isLoading).not.toBe(true); 286 | if (!authState.isAuthenticated.value) { 287 | return next(redirectTo(to, from)); 288 | } 289 | 290 | next(); 291 | }); 292 | }; 293 | 294 | const setupAuth0 = (app: App, convex: ConvexVueClient, options: Auth0Options) => { 295 | const { isAuthenticated, isLoading, getAccessTokenSilently } = 296 | app.config.globalProperties.$auth0; 297 | 298 | const isConvexAuthenticated = ref(false); 299 | const isConvexAuthLoading = ref(isLoading.value); 300 | 301 | const fetchAccessToken = async ({ 302 | forceRefreshToken 303 | }: { 304 | forceRefreshToken: boolean; 305 | }) => { 306 | try { 307 | const response = await getAccessTokenSilently({ 308 | detailedResponse: true, 309 | cacheMode: forceRefreshToken ? 'off' : 'on' 310 | }); 311 | return response.id_token as string; 312 | } catch (error) { 313 | return null; 314 | } 315 | }; 316 | 317 | const syncConvexAuthWithAuth0Auth = () => { 318 | if (!isConvexAuthLoading.value && isLoading.value) { 319 | isConvexAuthLoading.value = true; 320 | } 321 | 322 | if (isLoading.value) return; 323 | if (isAuthenticated.value) { 324 | convex.setAuth(fetchAccessToken, isAuth => { 325 | isConvexAuthenticated.value = isAuth; 326 | isConvexAuthLoading.value = false; 327 | }); 328 | } else { 329 | convex.clearAuth(); 330 | isConvexAuthenticated.value = false; 331 | isConvexAuthLoading.value = false; 332 | } 333 | }; 334 | 335 | watchEffect(syncConvexAuthWithAuth0Auth); 336 | 337 | const authState = { 338 | isLoading: readonly(isConvexAuthLoading), 339 | isAuthenticated: readonly(isConvexAuthenticated) 340 | }; 341 | app.provide(CONVEX_AUTH_INJECTION_KEY, authState); 342 | 343 | if (options.installNavigationGuard) { 344 | installNavigationGuard(authState, app.config.globalProperties.$router, options); 345 | } 346 | }; 347 | 348 | export const createConvex = ( 349 | origin: string, 350 | options: CreateConvexOptions = {} 351 | ): Plugin => ({ 352 | install(app) { 353 | const convex = new ConvexVueClient(origin); 354 | app.provide(CONVEX_INJECTION_KEY, convex); 355 | 356 | if (options.auth0) { 357 | setupAuth0(app, convex, options.auth0); 358 | } 359 | } 360 | }); 361 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @layer base, components, utilities; 2 | 3 | @import url('open-props/postcss/normalize') layer(base); 4 | @import 'open-props/postcss/style'; 5 | @import 'open-props/colors-hsl'; 6 | @import url('./reset.css') layer(base); 7 | @import url('./utils.css') layer(utilities); 8 | @import './theme.css'; 9 | 10 | @layer base { 11 | @unocss preflights; 12 | } 13 | 14 | @layer utilities { 15 | @unocss default; 16 | @unocss; 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | :where(:focus-visible) { 2 | outline-width: 2px; 3 | } 4 | 5 | ul { 6 | list-style: none; 7 | padding: 0; 8 | font-size: inherit; 9 | } 10 | 11 | ul > li { 12 | padding: 0; 13 | max-inline-size: 100%; 14 | } 15 | 16 | p { 17 | max-inline-size: 100%; 18 | } 19 | 20 | :is(a[href], a[href]:hover, a[href]:visited) { 21 | color: inherit; 22 | text-decoration: none; 23 | } 24 | 25 | input:-webkit-autofill, 26 | input:-webkit-autofill:hover, 27 | input:-webkit-autofill:focus, 28 | textarea:-webkit-autofill, 29 | textarea:-webkit-autofill:hover, 30 | textarea:-webkit-autofill:focus, 31 | select:-webkit-autofill, 32 | select:-webkit-autofill:hover, 33 | select:-webkit-autofill:focus { 34 | -webkit-text-fill-color: var(--text-1); 35 | -webkit-box-shadow: 0 0 0px 1000px var(--surface-1) inset; 36 | } 37 | 38 | svg *:focus { 39 | outline: none; 40 | } 41 | 42 | fieldset { 43 | padding: 0; 44 | border: none; 45 | border-radius: 0; 46 | } 47 | 48 | :is(h1, h2, h3, h4, h5, h6) { 49 | font-weight: var(--font-weight-7); 50 | } 51 | 52 | body { 53 | background: var(--surface-2); 54 | color: var(--text-1); 55 | overscroll-behavior-y: contain; 56 | } 57 | 58 | :is(button, a):focus-visible { 59 | outline: var(--link) solid 3px; 60 | transition: outline-offset 145ms var(--ease-2); 61 | } 62 | 63 | :where(button) { 64 | cursor: pointer; 65 | background-color: transparent; 66 | border: none; 67 | } 68 | 69 | :where(:not(:active):focus-visible) { 70 | outline-offset: var(--outline-offset); 71 | } 72 | 73 | @media (--mouse) { 74 | a:hover { 75 | text-decoration: underline; 76 | } 77 | } 78 | 79 | html.dark { 80 | color-scheme: dark; 81 | } 82 | -------------------------------------------------------------------------------- /src/styles/theme.css: -------------------------------------------------------------------------------- 1 | /* 2 | theme colors 3 | These colors gets picked up by the uno config at compile time to generate dynamic colors 4 | They MUST start woth --color- AND end with -hsl. They also need to be hsl values NON COMMA SEPARATED 5 | You don't need to use these directly in your css files (see below) 6 | */ 7 | :root { 8 | --color-text-1-hsl: var(--gray-11-hsl); 9 | --color-text-2-hsl: var(--gray-8-hsl); 10 | --color-text-3-hsl: var(--gray-7-hsl); 11 | 12 | --color-surface-1-hsl: 0 0% 100%; 13 | --color-surface-2-hsl: var(--gray-0-hsl); 14 | --color-surface-3-hsl: var(--gray-1-hsl); 15 | --color-surface-4-hsl: var(--gray-2-hsl); 16 | 17 | --color-primary-hsl: var(--gray-11-hsl); 18 | --color-primary-hover-hsl: var(--gray-8-hsl); 19 | --color-text-on-primary-hsl: var(--gray-0-hsl); 20 | 21 | --color-error-hsl: var(--red-7-hsl); 22 | --color-error-hover-hsl: var(--red-8-hsl); 23 | --color-text-on-error-hsl: var(--gray-11-hsl); 24 | 25 | --color-disabled-hsl: var(--gray-3-hsl); 26 | --color-text-on-disabled-hsl: var(--gray-6-hsl); 27 | 28 | --color-link-hsl: var(--violet-7-hsl); 29 | } 30 | 31 | /* Theme color Dark mode overrideq */ 32 | html.dark { 33 | --color-text-1-hsl: var(--gray-3-hsl); 34 | --color-text-2-hsl: var(--gray-5-hsl); 35 | --color-text-3-hsl: var(--gray-6-hsl); 36 | 37 | --color-surface-1-hsl: var(--gray-9-hsl); 38 | --color-surface-2-hsl: var(--gray-10-hsl); 39 | --color-surface-3-hsl: var(--gray-11-hsl); 40 | --color-surface-4-hsl: var(--gray-12-hsl); 41 | 42 | --color-primary-hsl: var(--violet-9-hsl); 43 | --color-primary-hover-hsl: var(--violet-8-hsl); 44 | 45 | --color-error-hsl: var(--pink-5-hsl); 46 | --color-error-hover-hsl: var(--pink-4-hsl); 47 | 48 | --color-disabled-hsl: var(--gray-8-hsl); 49 | --color-text-on-disabled-hsl: var(--gray-6-hsl); 50 | 51 | --color-link-hsl: var(--violet-5-hsl); 52 | } 53 | 54 | :root { 55 | /* 56 | Color aliases 57 | These are the colors you should actually be using in your css files 58 | */ 59 | --text-1: hsl(var(--color-text-1-hsl)); 60 | --text-2: hsl(var(--color-text-2-hsl)); 61 | --text-3: hsl(var(--color-text-3-hsl)); 62 | 63 | --surface-1: hsl(var(--color-surface-1-hsl)); 64 | --surface-2: hsl(var(--color-surface-2-hsl)); 65 | --surface-3: hsl(var(--color-surface-3-hsl)); 66 | --surface-4: hsl(var(--color-surface-3-hsl)); 67 | 68 | --primary: hsl(var(--color-primary-hsl)); 69 | --primary-hover: hsl(var(--color-primary-hover-hsl)); 70 | --text-on-primary: hsl(var(--color-text-on-primary-hsl)); 71 | 72 | --error: hsl(var(--color-error-hsl)); 73 | --error-hover: hsl(var(--color-error-hover-hsl)); 74 | --text-on-error: hsl(var(--color-text-on-error-hsl)); 75 | 76 | --disabled: hsl(var(--color-disabled-hsl)); 77 | --text-on-disabled: hsl(var(--color-text-on-disabled-hsl)); 78 | 79 | --border-dimmed: hsl(var(--gray-11-hsl) / 0.15); 80 | --border: hsl(var(--gray-11-hsl) / 0.4); 81 | 82 | --link: hsl(var(--color-link-hsl)); 83 | 84 | --transparent: transparent; 85 | 86 | /* Additional values not present in Open props */ 87 | --outline-offset: 3px; 88 | --radius-pill: 9999px; 89 | --size-05: 0.125rem; 90 | --font-size-0: 0.9rem; 91 | --font-size-00: 0.8rem; 92 | --font-size-000: 0.7rem; 93 | --font-size-9: 4rem; 94 | --font-size-10: 5rem; 95 | --font-size-11: 6rem; 96 | --font-size-12: 7.5rem; 97 | --size-content-4: 80ch; 98 | 99 | --size-05-em: 0.125em; 100 | --size-1-em: 0.25em; 101 | --size-2-em: 0.5em; 102 | --size-3-em: 1em; 103 | --size-4-em: 1.25em; 104 | --size-5-em: 1.5em; 105 | --size-6-em: 1.75em; 106 | --size-7-em: 2em; 107 | --size-8-em: 3em; 108 | --size-9-em: 4em; 109 | --size-10-em: 5em; 110 | --size-11-em: 7.5em; 111 | --size-12-em: 10em; 112 | --size-13-em: 15em; 113 | --size-14-em: 20em; 114 | --size-15-em: 30em; 115 | 116 | /* Override breakpoint variables to use rem units */ 117 | --size-xxs: 15rem; 118 | --size-xs: 23rem; 119 | --size-sm: 30rem; 120 | --size-md: 48rem; 121 | --size-lg: 64rem; 122 | --size-xl: 90rem; 123 | --size-xxl: 120rem; 124 | } 125 | 126 | html.dark { 127 | --border-dimmed: hsl(var(--gray-0-hsl) / 0.15); 128 | --border: hsl(var(--gray-0-hsl) / 0.4); 129 | 130 | /* used internally by open-props box-shadows */ 131 | --shadow-color: 210 7% 90%; 132 | } 133 | -------------------------------------------------------------------------------- /src/styles/utils.css: -------------------------------------------------------------------------------- 1 | .sr-only { 2 | position: absolute !important; 3 | width: 1px !important; 4 | height: 1px !important; 5 | padding: 0 !important; 6 | margin: -1px !important; 7 | overflow: hidden !important; 8 | clip: rect(0, 0, 0, 0) !important; 9 | white-space: nowrap !important; 10 | border: 0 !important; 11 | } 12 | 13 | .container { 14 | width: 100%; 15 | margin-inline: auto; 16 | max-width: var(--container-size, var(--size-lg)); 17 | } 18 | 19 | .surface { 20 | background-color: var(--surface-1); 21 | color: var(--text-1); 22 | padding: var(--size-6); 23 | } 24 | 25 | .center { 26 | height: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | -------------------------------------------------------------------------------- /src/sw.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cleanupOutdatedCaches, 3 | createHandlerBoundToURL, 4 | precacheAndRoute 5 | } from 'workbox-precaching'; 6 | import { NavigationRoute, registerRoute } from 'workbox-routing'; 7 | 8 | declare let self: ServiceWorkerGlobalScope & { 9 | addEventListener: any; 10 | skipWaiting: any; 11 | }; 12 | 13 | self.addEventListener('message', (event: any) => { 14 | if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting(); 15 | }); 16 | 17 | // self.__WB_MANIFEST is default injection point 18 | precacheAndRoute(self.__WB_MANIFEST); 19 | 20 | // clean old assets 21 | cleanupOutdatedCaches(); 22 | 23 | // to allow work offline 24 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))); 25 | -------------------------------------------------------------------------------- /src/utils/assertions.ts: -------------------------------------------------------------------------------- 1 | import type { Defined, Nullable } from './types'; 2 | 3 | export const isObject = (x: unknown): x is object => 4 | typeof x === 'object' && x !== null && !Array.isArray(x); 5 | 6 | export const isString = (x: unknown): x is string => typeof x === 'string'; 7 | 8 | export const isNumber = (x: unknown): x is number => typeof x === 'number'; 9 | 10 | export const isBoolean = (x: unknown): x is boolean => x === true || x === false; 11 | 12 | export const isDefined = (arg: Nullable): arg is Defined => 13 | arg !== undefined && arg !== null; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 16 | export const isNever = (x: never) => { 17 | throw new Error('Missing case in exhaustive switch'); 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/convex/QueriesObserver.ts: -------------------------------------------------------------------------------- 1 | import { convexToJson, type Value } from 'convex/values'; 2 | import type { QueryJournal } from 'convex/browser'; 3 | import type { Watch } from '@/plugins/convex'; 4 | import { type FunctionReference, getFunctionName } from 'convex/server'; 5 | 6 | type Identifier = string; 7 | 8 | type QueryInfo = { 9 | query: FunctionReference<'query'>; 10 | args: Record; 11 | watch: Watch; 12 | unsubscribe: () => void; 13 | }; 14 | 15 | export type CreateWatch = ( 16 | query: FunctionReference<'query'>, 17 | args: Record, 18 | journal?: QueryJournal 19 | ) => Watch; 20 | 21 | /** 22 | * A class for observing the results of multiple queries at the same time. 23 | * 24 | * Any time the result of a query changes, the listeners are notified. 25 | */ 26 | export class QueriesObserver { 27 | public createWatch: CreateWatch; 28 | private queries: Record; 29 | private listeners: Set<() => void>; 30 | 31 | constructor(createWatch: CreateWatch) { 32 | this.createWatch = createWatch; 33 | this.queries = {}; 34 | this.listeners = new Set(); 35 | } 36 | 37 | setQueries( 38 | newQueries: Record< 39 | Identifier, 40 | { query: FunctionReference<'query'>; args: Record } 41 | > 42 | ) { 43 | // Add the new queries before unsubscribing from the old ones so that 44 | // the deduping in the `ConvexReactClient` can help if there are duplicates. 45 | for (const identifier of Object.keys(newQueries)) { 46 | const { query, args } = newQueries[identifier]; 47 | if (!getFunctionName(query)) { 48 | throw new Error(`query ${name} is not a FunctionReference`); 49 | } 50 | 51 | if (this.queries[identifier] === undefined) { 52 | // No existing query => add it. 53 | this.addQuery(identifier, query, args); 54 | } else { 55 | const existingInfo = this.queries[identifier]; 56 | if ( 57 | getFunctionName(query) !== getFunctionName(existingInfo.query) || 58 | JSON.stringify(convexToJson(args)) !== 59 | JSON.stringify(convexToJson(existingInfo.args)) 60 | ) { 61 | // Existing query that doesn't match => remove the old and add the new. 62 | this.removeQuery(identifier); 63 | this.addQuery(identifier, query, args); 64 | } 65 | } 66 | } 67 | 68 | // Prune all the existing queries that we no longer need. 69 | for (const identifier of Object.keys(this.queries)) { 70 | if (newQueries[identifier] === undefined) { 71 | this.removeQuery(identifier); 72 | } 73 | } 74 | } 75 | 76 | subscribe(listener: () => void): () => void { 77 | this.listeners.add(listener); 78 | return () => { 79 | this.listeners.delete(listener); 80 | }; 81 | } 82 | 83 | getCurrentQueries(): Record { 84 | const result: Record = {}; 85 | for (const identifier of Object.keys(this.queries)) { 86 | let value: Value | undefined | Error; 87 | try { 88 | value = this.queries[identifier].watch.localQueryResult(); 89 | } catch (e) { 90 | // Only collect instances of `Error` because thats how callers 91 | // will distinguish errors from normal results. 92 | if (e instanceof Error) { 93 | value = e; 94 | } else { 95 | throw e; 96 | } 97 | } 98 | result[identifier] = value; 99 | } 100 | return result; 101 | } 102 | 103 | setCreateWatch(createWatch: CreateWatch) { 104 | this.createWatch = createWatch; 105 | // If we have a new watch, we might be using a new Convex client. 106 | // Recreate all the watches being careful to preserve the journals. 107 | for (const identifier of Object.keys(this.queries)) { 108 | const { query, args, watch } = this.queries[identifier]; 109 | const journal = watch.journal(); 110 | this.removeQuery(identifier); 111 | this.addQuery(identifier, query, args, journal); 112 | } 113 | } 114 | 115 | destroy() { 116 | for (const identifier of Object.keys(this.queries)) { 117 | this.removeQuery(identifier); 118 | } 119 | this.listeners = new Set(); 120 | } 121 | 122 | private addQuery( 123 | identifier: Identifier, 124 | query: FunctionReference<'query'>, 125 | args: Record, 126 | journal?: QueryJournal 127 | ) { 128 | if (this.queries[identifier] !== undefined) { 129 | throw new Error( 130 | `Tried to add a new query with identifier ${identifier} when it already exists.` 131 | ); 132 | } 133 | const watch = this.createWatch(query, args, journal); 134 | const unsubscribe = watch.onUpdate(() => this.notifyListeners()); 135 | this.queries[identifier] = { 136 | query, 137 | args, 138 | watch, 139 | unsubscribe 140 | }; 141 | } 142 | 143 | private removeQuery(identifier: Identifier) { 144 | const info = this.queries[identifier]; 145 | if (info === undefined) { 146 | throw new Error(`No query found with identifier ${identifier}.`); 147 | } 148 | info.unsubscribe(); 149 | delete this.queries[identifier]; 150 | } 151 | 152 | private notifyListeners(): void { 153 | for (const listener of this.listeners) { 154 | listener(); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export const getFocusableChildren = (node?: HTMLElement | null | undefined) => 2 | node 3 | ? ([ 4 | ...node.querySelectorAll( 5 | ':where(button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])):not(:disabled)' 6 | ) 7 | ] as HTMLElement[]) 8 | : []; 9 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type Defined = Exclude; 2 | export type Nullable = T | null | undefined; 3 | export type PartialBy = Omit & Partial>; 4 | export type Entries = { [K in keyof T]: [K, T[K]] }[keyof T]; 5 | export type AnyObject = { [key: string]: any }; 6 | export type AnyFunction = (...args: any[]) => any; 7 | export type Keys = keyof T; 8 | export type Values = T[keyof T]; 9 | export type Override = Omit & B; 10 | export type AsyncReturnType Promise> = T extends ( 11 | ...args: any 12 | ) => Promise 13 | ? R 14 | : any; 15 | export type MaybePromise = T | Promise; 16 | /** 17 | * Hack! This type causes TypeScript to simplify how it renders object types. 18 | * 19 | * It is functionally the identity for object types, but in practice it can 20 | * simplify expressions like `A & B`. 21 | */ 22 | export type Expand> = ObjectType extends Record< 23 | any, 24 | any 25 | > 26 | ? { 27 | [Key in keyof ObjectType]: ObjectType[Key]; 28 | } 29 | : never; 30 | 31 | /** 32 | * An `Omit<>` type that: 33 | * 1. Applies to each element of a union. 34 | * 2. Preserves the index signature of the underlying type. 35 | */ 36 | export type BetterOmit = { 37 | [Property in keyof T as Property extends K ? never : Property]: T[Property]; 38 | }; 39 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-recommended-vue', 4 | 'stylelint-config-html', 5 | 'stylelint-config-clean-order' 6 | ], 7 | rules: { 8 | 'no-descending-specificity': null, 9 | 'selector-pseudo-class-no-unknown': [ 10 | true, 11 | { 12 | ignorePseudoClasses: ['deep', 'global'] 13 | } 14 | ], 15 | 'selector-pseudo-element-no-unknown': [ 16 | true, 17 | { 18 | ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'] 19 | } 20 | ], 21 | 'at-rule-no-unknown': [ 22 | true, 23 | { 24 | ignoreAtRules: [ 25 | 'apply', 26 | 'config', 27 | 'layer', 28 | 'responsive', 29 | 'screen', 30 | 'tailwind', 31 | 'unocss', 32 | 'variants' 33 | ] 34 | } 35 | ], 36 | 'function-no-unknown': [ 37 | true, 38 | { 39 | ignoreFunctions: ['theme', 'v-bind'] 40 | } 41 | ] 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /tools/ark-ui-resolver.ts: -------------------------------------------------------------------------------- 1 | import { type Resolver } from 'unplugin-auto-import/types'; 2 | 3 | export interface ModuleOptions { 4 | prefix: string; 5 | } 6 | 7 | export const ArkUiResolver: Resolver = componentName => { 8 | if (componentName.startsWith('Ark')) 9 | return { name: componentName.slice(3), from: '@ark-ui/vue' }; 10 | }; 11 | -------------------------------------------------------------------------------- /tools/uno-openprops-preset.ts: -------------------------------------------------------------------------------- 1 | // uno.config.ts 2 | import { 3 | definePreset, 4 | type CSSObject, 5 | type DynamicMatcher, 6 | type RuleContext, 7 | type CSSColorValue, 8 | escapeRegExp 9 | } from 'unocss'; 10 | import { parseColor } from '@unocss/preset-mini'; 11 | import openProps from 'open-props'; 12 | 13 | // prettier-ignore 14 | const cssColorFunctions = ['hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch', 'rgb', 'rgba'] 15 | const alphaPlaceholders = ['%alpha', '']; 16 | const alphaPlaceholdersRE = new RegExp( 17 | alphaPlaceholders.map(v => escapeRegExp(v)).join('|') 18 | ); 19 | const numberWithUnitRE = 20 | /^(-?\d*(?:\.\d+)?)(px|pt|pc|%|r?(?:em|ex|lh|cap|ch|ic)|(?:[sld]?v|cq)(?:[whib]|min|max)|in|cm|mm|rpx)?$/i; 21 | const globalKeywords = ['inherit', 'initial', 'revert', 'revert-layer', 'unset']; 22 | const directionMap: Record = { 23 | l: ['-left'], 24 | r: ['-right'], 25 | t: ['-top'], 26 | b: ['-bottom'], 27 | s: ['-inline-start'], 28 | e: ['-inline-end'], 29 | x: ['-left', '-right'], 30 | y: ['-top', '-bottom'], 31 | '': [''], 32 | bs: ['-block-start'], 33 | be: ['-block-end'], 34 | is: ['-inline-start'], 35 | ie: ['-inline-end'], 36 | block: ['-block-start', '-block-end'], 37 | inline: ['-inline-start', '-inline-end'] 38 | }; 39 | 40 | export function hasParseableColor(color: string | undefined, theme: object) { 41 | return color != null && !!parseColor(color, theme)?.color; 42 | } 43 | 44 | export function colorOpacityToString(color: CSSColorValue) { 45 | const alpha = color.alpha ?? 1; 46 | return typeof alpha === 'string' && alphaPlaceholders.includes(alpha) ? 1 : alpha; 47 | } 48 | 49 | export function colorToString( 50 | color: CSSColorValue | string, 51 | alphaOverride?: string | number 52 | ) { 53 | if (typeof color === 'string') 54 | return color.replace(alphaPlaceholdersRE, `${alphaOverride ?? 1}`); 55 | 56 | const { components } = color; 57 | let { alpha, type } = color; 58 | alpha = alphaOverride ?? alpha; 59 | type = type.toLowerCase(); 60 | 61 | alpha = alpha == null ? '' : ` / ${alpha}`; 62 | if (cssColorFunctions.includes(type)) return `${type}(${components.join(' ')}${alpha})`; 63 | return `color(${type} ${components.join(' ')}${alpha})`; 64 | } 65 | 66 | export function colorResolver( 67 | property: string, 68 | varName: string, 69 | shouldPass?: (css: CSSObject) => boolean 70 | ): DynamicMatcher { 71 | return ([, body]: string[], { theme }: RuleContext): CSSObject | undefined => { 72 | const data = parseColor(body, theme); 73 | if (!data) return; 74 | 75 | const { alpha, color, cssColor } = data; 76 | 77 | const css: CSSObject = {}; 78 | if (cssColor) { 79 | if (alpha != null) { 80 | css[property] = colorToString(cssColor, alpha); 81 | } else { 82 | css[`--un-${varName}-opacity`] = colorOpacityToString(cssColor); 83 | css[property] = colorToString(cssColor, `var(--un-${varName}-opacity)`); 84 | } 85 | } else if (color) { 86 | css[property] = colorToString(color, alpha); 87 | } 88 | 89 | if (shouldPass?.(css) !== false) return css; 90 | }; 91 | } 92 | 93 | function borderColorResolver(direction: string) { 94 | return ([, body]: string[], theme: object): CSSObject | undefined => { 95 | const data = parseColor(body, theme); 96 | 97 | if (!data) return; 98 | 99 | const { alpha, color, cssColor } = data; 100 | 101 | if (cssColor) { 102 | if (alpha != null) { 103 | return { 104 | [`border${direction}-color`]: colorToString(cssColor, alpha) 105 | }; 106 | } 107 | if (direction === '') { 108 | return { 109 | '--un-border-opacity': colorOpacityToString(cssColor), 110 | 'border-color': colorToString(cssColor, 'var(--un-border-opacity)') 111 | }; 112 | } else { 113 | return { 114 | // Separate this return since if `direction` is an empty string, the first key will be overwritten by the second. 115 | '--un-border-opacity': colorOpacityToString(cssColor), 116 | [`--un-border${direction}-opacity`]: 'var(--un-border-opacity)', 117 | [`border${direction}-color`]: colorToString( 118 | cssColor, 119 | `var(--un-border${direction}-opacity)` 120 | ) 121 | }; 122 | } 123 | } else if (color) { 124 | return { 125 | [`border${direction}-color`]: colorToString(color, alpha) 126 | }; 127 | } 128 | }; 129 | } 130 | 131 | function handlerBorderColor( 132 | [, a = '', c]: string[], 133 | { theme }: RuleContext 134 | ): CSSObject | undefined { 135 | if (a in directionMap && hasParseableColor(c, theme)) { 136 | return Object.assign( 137 | {}, 138 | ...directionMap[a].map(i => borderColorResolver(i)(['', c], theme)) 139 | ); 140 | } 141 | } 142 | 143 | const makeSwatch = (hue: string) => { 144 | const swatch: Record = {}; 145 | for (let i = 0; i < 13; i++) { 146 | // const key = `${hue}${i}` as keyof typeof openProps; 147 | swatch[i] = `hsl(var(--${hue}-${i}-hsl) / )`; 148 | } 149 | 150 | return swatch; 151 | }; 152 | //prettier-ignore 153 | const hues = ['gray','stone','red','pink','purple','violet','indigo','blue','cyan','teal','green','lime','yellow','orange','choco','brown','sand','jungle']; 154 | const sizes = ['000', '00', '05', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; 155 | const fontSizes = Object.fromEntries( 156 | ['000', '00', '0', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].map(s => [ 157 | s, 158 | `var(--font-size-${s})` 159 | ]) 160 | ); 161 | const colors = Object.fromEntries(hues.map(hue => [hue, makeSwatch(hue)])); 162 | const spacing = Object.fromEntries( 163 | sizes 164 | .map(size => [ 165 | [size, `var(--size-${size})`], 166 | [`${size}-em`, `var(--size-${size}-em)`] 167 | ]) 168 | .flat() 169 | ); 170 | 171 | export const presetOpenProps = () => 172 | definePreset({ 173 | name: 'open-props', 174 | 175 | theme: { 176 | colors, 177 | boxShadow: { 178 | 1: 'var(--shadow-1)', 179 | 2: 'var(--shadow-2)', 180 | 3: 'var(--shadow-3)', 181 | 4: 'var(--shadow-4)', 182 | 5: 'var(--shadow-5)', 183 | 6: 'var(--shadow-6)', 184 | 'inner-0': 'var(--inner-shadow-1)', 185 | 'inner-1': 'var(--inner-shadow-1)', 186 | 'inner-2': 'var(--inner-shadow-1)', 187 | 'inner-3': 'var(--inner-shadow-1)', 188 | 'inner-4': 'var(--inner-shadow-1)' 189 | }, 190 | borderRadius: { 191 | 1: 'var(--radius-1)', 192 | 2: 'var(--radius-2)', 193 | 3: 'var(--radius-3)', 194 | 4: 'var(--radius-4)', 195 | 5: 'var(--radius-5)', 196 | 6: 'var(--radius-6)', 197 | 'blob-1': 'var(--radius-blob-1)', 198 | 'blob-2': 'var(--radius-blob-2)', 199 | 'blob-3': 'var(--radius-blob-3)', 200 | 'blob-4': 'var(--radius-blob-4)', 201 | 'blob-5': 'var(--radius-blob-5)', 202 | 'conditional-1': 'var(--radius-conditional-1)', 203 | 'conditional-2': 'var(--radius-conditional-2)', 204 | 'conditional-3': 'var(--radius-conditional-3)', 205 | 'conditional-4': 'var(--radius-conditional-4)', 206 | 'conditional-5': 'var(--radius-conditional-5)', 207 | 'conditional-6': 'var(--radius-conditional-6)', 208 | round: 'var(--radius-round)', 209 | pill: 'var(--radius-pill)' 210 | }, 211 | spacing: { 212 | ...spacing, 213 | 'content-1': 'var(--size-content-1)', 214 | 'content-2': 'var(--size-content-2)', 215 | 'content-3': 'var(--size-content-3)', 216 | 'content-4': 'var(--size-content-4)', 217 | '1-fluid': 'var(--size-1-fluid)', 218 | '2-fluid': 'var(--size-2-fluid)', 219 | '3-fluid': 'var(--size-3-fluid)', 220 | '4-fluid': 'var(--size-4-fluid)', 221 | '5-fluid': 'var(--size-5-fluid)', 222 | '6-fluid': 'var(--size-6-fluid)', 223 | '7-fluid': 'var(--size-7-fluid)', 224 | '8-fluid': 'var(--size-8-fluid)', 225 | '9-fluid': 'var(--size-9-fluid)', 226 | '10-fluid': 'var(--size-10-fluid)', 227 | 228 | xxs: 'var(--size-xxs)', 229 | xs: 'var(--size-xxs)', 230 | sm: 'var(--size-xxs)', 231 | md: 'var(--size-xxs)', 232 | lg: 'var(--size-xxs)', 233 | xl: 'var(--size-xxs)', 234 | xxl: 'var(--size-xxs)' 235 | }, 236 | fontSize: { 237 | ...fontSizes, 238 | '0-fluid': 'var(--font-size-fluid-0)', 239 | '1-fluid': 'var(--font-size-fluid-1)', 240 | '2-fluid': 'var(--font-size-fluid-2)', 241 | '3-fluid': 'var(--font-size-fluid-3)' 242 | }, 243 | breakpoints: { 244 | xxs: '15em', 245 | xs: '23em', 246 | sm: '30em', 247 | md: '48em', 248 | lg: '64em', 249 | xl: '90em', 250 | xxl: '120em' 251 | } 252 | }, 253 | 254 | rules: [ 255 | // open-props gradients 256 | [/^gradient-(\d+)$/, ([, d]) => ({ 'background-image': `var(--gradient-${d})` })], 257 | // open-props aspect ratio 258 | [ 259 | /^aspect-(\d+)$/, 260 | ([, d]) => { 261 | const val = openProps[`aspect${d}` as keyof typeof openProps] as 262 | | string 263 | | undefined; 264 | return { 265 | 'aspect-ratio': val ?? d 266 | }; 267 | } 268 | ], 269 | // open-props colors 270 | [ 271 | /^(?:color|c)-(.+)$/, 272 | colorResolver('color', 'text'), 273 | { autocomplete: '(color|c)-$colors' } 274 | ], 275 | // auto detection and fallback to font-size if the content looks like a size 276 | [ 277 | /^text-(.+)$/, 278 | colorResolver( 279 | 'color', 280 | 'text', 281 | css => !css.color?.toString().match(numberWithUnitRE) 282 | ), 283 | { autocomplete: 'text-$colors' } 284 | ], 285 | [ 286 | /^(?:text|color|c)-(.+)$/, 287 | ([, v]) => (globalKeywords.includes(v) ? { color: v } : undefined), 288 | { autocomplete: `(text|color|c)-(${globalKeywords.join('|')})` } 289 | ], 290 | 291 | [ 292 | /^bg-(.+)$/, 293 | colorResolver('background-color', 'bg'), 294 | { autocomplete: 'bg-$colors' } 295 | ], 296 | // border-color 297 | [ 298 | /^(?:border|b)-()(?:color-)?(.+)$/, 299 | handlerBorderColor, 300 | { autocomplete: ['(border|b)-$colors', '(border|b)--$colors'] } 301 | ], 302 | [/^(?:border|b)-([xy])-(?:color-)?(.+)$/, handlerBorderColor], 303 | [/^(?:border|b)-([rltbse])-(?:color-)?(.+)$/, handlerBorderColor], 304 | [/^(?:border|b)-(block|inline)-(?:color-)?(.+)$/, handlerBorderColor], 305 | [/^(?:border|b)-([bi][se])-(?:color-)?(.+)$/, handlerBorderColor] 306 | ] 307 | }); 308 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.vue", 7 | "tools/**/*.ts", 8 | "auto-imports.d.ts", 9 | "components.d.ts", 10 | "typed-router.d.ts" 11 | ], 12 | "exclude": ["src/**/__tests__/*"], 13 | "compilerOptions": { 14 | "composite": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": ["./src/*"], 18 | "api": ["./convex/_generated/api"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "tools/**/*.ts", 5 | "vite.config.*", 6 | "vitest.config.*", 7 | "cypress.config.*", 8 | "nightwatch.conf.*", 9 | "playwright.config.*" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "types": ["node"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /typed-router.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️ 5 | // It's recommended to commit this file. 6 | // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. 7 | 8 | /// 9 | 10 | import type { 11 | // type safe route locations 12 | RouteLocationTypedList, 13 | RouteLocationResolvedTypedList, 14 | RouteLocationNormalizedTypedList, 15 | RouteLocationNormalizedLoadedTypedList, 16 | RouteLocationAsString, 17 | RouteLocationAsRelativeTypedList, 18 | RouteLocationAsPathTypedList, 19 | 20 | // helper types 21 | // route definitions 22 | RouteRecordInfo, 23 | ParamValue, 24 | ParamValueOneOrMore, 25 | ParamValueZeroOrMore, 26 | ParamValueZeroOrOne, 27 | 28 | // vue-router extensions 29 | _RouterTyped, 30 | RouterLinkTyped, 31 | RouterLinkPropsTyped, 32 | NavigationGuard, 33 | UseLinkFnTyped, 34 | 35 | // data fetching 36 | _DataLoader, 37 | _DefineLoaderOptions, 38 | } from 'unplugin-vue-router/types' 39 | 40 | declare module 'vue-router/auto/routes' { 41 | export interface RouteNamedMap { 42 | 'Home': RouteRecordInfo<'Home', '/', Record, Record>, 43 | 'Profile': RouteRecordInfo<'Profile', '/profile/:id', { id: ParamValue }, { id: ParamValue }>, 44 | } 45 | } 46 | 47 | declare module 'vue-router/auto' { 48 | import type { RouteNamedMap } from 'vue-router/auto/routes' 49 | 50 | export type RouterTyped = _RouterTyped 51 | 52 | /** 53 | * Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards). 54 | * Allows passing the name of the route to be passed as a generic. 55 | */ 56 | export type RouteLocationNormalized = RouteLocationNormalizedTypedList[Name] 57 | 58 | /** 59 | * Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`). 60 | * Allows passing the name of the route to be passed as a generic. 61 | */ 62 | export type RouteLocationNormalizedLoaded = RouteLocationNormalizedLoadedTypedList[Name] 63 | 64 | /** 65 | * Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`). 66 | * Allows passing the name of the route to be passed as a generic. 67 | */ 68 | export type RouteLocationResolved = RouteLocationResolvedTypedList[Name] 69 | 70 | /** 71 | * Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic. 72 | */ 73 | export type RouteLocation = RouteLocationTypedList[Name] 74 | 75 | /** 76 | * Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic. 77 | */ 78 | export type RouteLocationRaw = 79 | | RouteLocationAsString 80 | | RouteLocationAsRelativeTypedList[Name] 81 | | RouteLocationAsPathTypedList[Name] 82 | 83 | /** 84 | * Generate a type safe params for a route location. Requires the name of the route to be passed as a generic. 85 | */ 86 | export type RouteParams = RouteNamedMap[Name]['params'] 87 | /** 88 | * Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic. 89 | */ 90 | export type RouteParamsRaw = RouteNamedMap[Name]['paramsRaw'] 91 | 92 | export function useRouter(): RouterTyped 93 | export function useRoute(name?: Name): RouteLocationNormalizedLoadedTypedList[Name] 94 | 95 | export const useLink: UseLinkFnTyped 96 | 97 | export function onBeforeRouteLeave(guard: NavigationGuard): void 98 | export function onBeforeRouteUpdate(guard: NavigationGuard): void 99 | 100 | export const RouterLink: RouterLinkTyped 101 | export const RouterLinkProps: RouterLinkPropsTyped 102 | 103 | // Experimental Data Fetching 104 | 105 | export function defineLoader< 106 | P extends Promise, 107 | Name extends keyof RouteNamedMap = keyof RouteNamedMap, 108 | isLazy extends boolean = false, 109 | >( 110 | name: Name, 111 | loader: (route: RouteLocationNormalizedLoaded) => P, 112 | options?: _DefineLoaderOptions, 113 | ): _DataLoader, isLazy> 114 | export function defineLoader< 115 | P extends Promise, 116 | isLazy extends boolean = false, 117 | >( 118 | loader: (route: RouteLocationNormalizedLoaded) => P, 119 | options?: _DefineLoaderOptions, 120 | ): _DataLoader, isLazy> 121 | 122 | export { 123 | _definePage as definePage, 124 | _HasDataLoaderMeta as HasDataLoaderMeta, 125 | _setupDataFetchingGuard as setupDataFetchingGuard, 126 | _stopDataFetchingScope as stopDataFetchingScope, 127 | } from 'unplugin-vue-router/runtime' 128 | } 129 | 130 | declare module 'vue-router' { 131 | import type { RouteNamedMap } from 'vue-router/auto/routes' 132 | 133 | export interface TypesConfig { 134 | beforeRouteUpdate: NavigationGuard 135 | beforeRouteLeave: NavigationGuard 136 | 137 | $route: RouteLocationNormalizedLoadedTypedList[keyof RouteNamedMap] 138 | $router: _RouterTyped 139 | 140 | RouterLink: RouterLinkTyped 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | // uno.config.ts 2 | import { 3 | defineConfig, 4 | presetUno, 5 | transformerVariantGroup, 6 | presetIcons, 7 | type Preset 8 | } from 'unocss'; 9 | import { presetOpenProps } from './tools/uno-openprops-preset'; 10 | import fs from 'fs'; 11 | import path from 'path'; 12 | import * as csstree from 'css-tree'; 13 | 14 | const cssTheme = fs.readFileSync(path.join(__dirname, 'src/styles/theme.css'), { 15 | encoding: 'utf-8' 16 | }); 17 | 18 | const ast = csstree.parse(cssTheme); 19 | const themeColors: Record = {}; 20 | const colorIdentifierRE = new RegExp('--color-(.+)-hsl$'); 21 | 22 | csstree.walk(ast, node => { 23 | if (node.type !== 'Declaration') return; 24 | const { property } = node; 25 | const match = property.match(colorIdentifierRE); 26 | if (match?.[1]) { 27 | themeColors[match[1]] = `hsl(var(${property}) / )`; 28 | } 29 | }); 30 | 31 | export default defineConfig({ 32 | blocklist: ['container'], 33 | presets: [presetIcons(), presetUno(), presetOpenProps() as Preset], 34 | transformers: [transformerVariantGroup()], 35 | theme: { 36 | colors: themeColors 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import { VueRouterAutoImports } from 'unplugin-vue-router'; 4 | import { fileURLToPath, URL } from 'node:url'; 5 | import AutoImport from 'unplugin-auto-import/vite'; 6 | import VueRouter from 'unplugin-vue-router/vite'; 7 | import Components from 'unplugin-vue-components/vite'; 8 | import { ArkUiResolver } from './tools/ark-ui-resolver'; 9 | import { VitePWA } from 'vite-plugin-pwa'; 10 | 11 | export default defineConfig({ 12 | plugins: [ 13 | VueRouter({ 14 | routesFolder: fileURLToPath(new URL('./src/pages', import.meta.url)), 15 | dts: './typed-router.d.ts' 16 | }), 17 | 18 | vue({ 19 | reactivityTransform: true, 20 | script: { 21 | defineModel: true 22 | } 23 | }), 24 | 25 | AutoImport({ 26 | include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/], 27 | imports: [ 28 | 'vue', 29 | '@vueuse/core', 30 | 'vee-validate', 31 | VueRouterAutoImports, 32 | { 33 | '@auth0/auth0-vue': ['useAuth0'] 34 | } 35 | ], 36 | dirs: ['./src/composables/**', './src/utils/**'] 37 | }), 38 | 39 | Components({ 40 | dts: true, 41 | extensions: ['vue'], 42 | globs: ['./src/components/**/*.vue', './src/directives/**/*.ts'], 43 | directoryAsNamespace: false, 44 | resolvers: [ArkUiResolver] 45 | }), 46 | 47 | VitePWA({ 48 | registerType: 'prompt', 49 | srcDir: 'src', 50 | filename: 'sw.ts', 51 | strategies: 'injectManifest', 52 | devOptions: { 53 | enabled: false, 54 | type: 'module' 55 | }, 56 | manifest: { 57 | name: 'Battle arena', 58 | short_name: 'BA', 59 | description: 'GOTY fr fr', 60 | theme_color: '#ffffff', 61 | icons: [ 62 | { 63 | src: '/icon/icon-192x192.png', 64 | sizes: '192x192', 65 | type: 'image/png' 66 | }, 67 | { 68 | src: '/icon/icon-512x512.png', 69 | sizes: '512x512', 70 | type: 'image/png' 71 | }, 72 | { 73 | src: '/icon/icon-512x512.png', 74 | sizes: '512x512', 75 | type: 'image/png', 76 | purpose: 'any maskable' 77 | } 78 | ] 79 | } 80 | }) 81 | ], 82 | resolve: { 83 | alias: { 84 | '@': fileURLToPath(new URL('./src', import.meta.url)) 85 | } 86 | }, 87 | server: { 88 | port: 3000 89 | } 90 | }); 91 | --------------------------------------------------------------------------------