diff --git a/.dockerignore b/.dockerignore index b689e34..bda43bf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,3 +21,4 @@ docker-compose* # App specific database/seeders +.adonisjs diff --git a/.gitignore b/.gitignore index 03b991c..7d99ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ yarn-error.log # Platform specific .DS_Store +.adonisjs diff --git a/Dockerfile b/Dockerfile index 9f2074a..0b02e58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,8 +37,7 @@ ENV DB_DATABASE=db_db ENV GOOGLE_CLIENT_ID=client_id ENV GOOGLE_CLIENT_SECRET=client_secret ENV GOOGLE_CLIENT_CALLBACK_URL=http://localhost:3333/auth/callback - -RUN node ace izzy:routes +RUN node ace tuyau:generate RUN node ace build # Production stage @@ -53,6 +52,7 @@ ENV PORT=$PORT WORKDIR /app COPY --from=production-deps /app/node_modules /app/node_modules COPY --from=build /app/build /app +COPY --from=build /app/.adonisjs /app/.adonisjs # Expose port EXPOSE $PORT diff --git a/adonisrc.ts b/adonisrc.ts index f39d7a1..58fe308 100644 --- a/adonisrc.ts +++ b/adonisrc.ts @@ -2,29 +2,29 @@ import { defineConfig } from '@adonisjs/core/app'; export default defineConfig({ /* - |-------------------------------------------------------------------------- - | Commands - |-------------------------------------------------------------------------- - | - | List of ace commands to register from packages. The application commands - | will be scanned automatically from the "./commands" directory. - | - */ +|-------------------------------------------------------------------------- +| Commands +|-------------------------------------------------------------------------- +| +| List of ace commands to register from packages. The application commands +| will be scanned automatically from the "./commands" directory. +| +*/ commands: [ () => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands'), - () => import('@izzyjs/route/commands'), + () => import('@tuyau/core/commands'), ], /* - |-------------------------------------------------------------------------- - | Service providers - |-------------------------------------------------------------------------- - | - | List of service providers to import and register when booting the - | application - | - */ +|-------------------------------------------------------------------------- +| Service providers +|-------------------------------------------------------------------------- +| +| List of service providers to import and register when booting the +| application +| +*/ providers: [ () => import('@adonisjs/core/providers/app_provider'), () => import('@adonisjs/core/providers/hash_provider'), @@ -43,29 +43,29 @@ export default defineConfig({ () => import('@adonisjs/auth/auth_provider'), () => import('@adonisjs/inertia/inertia_provider'), () => import('@adonisjs/ally/ally_provider'), - () => import('@izzyjs/route/izzy_provider'), () => import('#providers/route_provider'), + () => import('@tuyau/core/tuyau_provider'), ], /* - |-------------------------------------------------------------------------- - | Preloads - |-------------------------------------------------------------------------- - | - | List of modules to import before starting the application. - | - */ +|-------------------------------------------------------------------------- +| Preloads +|-------------------------------------------------------------------------- +| +| List of modules to import before starting the application. +| +*/ preloads: [() => import('#start/routes'), () => import('#start/kernel')], /* - |-------------------------------------------------------------------------- - | Tests - |-------------------------------------------------------------------------- - | - | List of test suites to organize tests by their type. Feel free to remove - | and add additional suites. - | - */ +|-------------------------------------------------------------------------- +| Tests +|-------------------------------------------------------------------------- +| +| List of test suites to organize tests by their type. Feel free to remove +| and add additional suites. +| +*/ tests: { suites: [ { @@ -83,14 +83,14 @@ export default defineConfig({ }, /* - |-------------------------------------------------------------------------- - | Metafiles - |-------------------------------------------------------------------------- - | - | A collection of files you want to copy to the build folder when creating - | the production build. - | - */ +|-------------------------------------------------------------------------- +| Metafiles +|-------------------------------------------------------------------------- +| +| A collection of files you want to copy to the build folder when creating +| the production build. +| +*/ metaFiles: [ { pattern: 'resources/views/**/*.edge', @@ -105,6 +105,5 @@ export default defineConfig({ assetsBundler: false, unstable_assembler: { onBuildStarting: [() => import('@adonisjs/vite/build_hook')], - onDevServerStarted: [() => import('@izzyjs/route/dev_hook')], }, }); diff --git a/app/controllers/searches_controller.ts b/app/controllers/searches_controller.ts index 91db28e..69a7ce9 100644 --- a/app/controllers/searches_controller.ts +++ b/app/controllers/searches_controller.ts @@ -1,16 +1,12 @@ +import { searchTermValidator } from '#validators/search_term'; import type { HttpContext } from '@adonisjs/core/http'; import db from '@adonisjs/lucid/services/db'; export default class SearchesController { async search({ request, auth }: HttpContext) { - const term = request.qs()?.term; - if (!term) { - console.warn('qs term null'); - return { error: 'missing "term" query param' }; - } - + const { searchTerm } = await request.validateUsing(searchTermValidator); const { rows } = await db.rawQuery('SELECT * FROM search_text(?, ?)', [ - term, + searchTerm, auth.user!.id, ]); return { results: rows }; diff --git a/app/controllers/users_controller.ts b/app/controllers/users_controller.ts index c8df55f..39c06dc 100644 --- a/app/controllers/users_controller.ts +++ b/app/controllers/users_controller.ts @@ -1,8 +1,8 @@ import User from '#models/user'; +import { RouteName } from '#types/tuyau'; import type { HttpContext } from '@adonisjs/core/http'; import logger from '@adonisjs/core/services/logger'; import db from '@adonisjs/lucid/services/db'; -import { RouteName } from '@izzyjs/route/types'; export default class UsersController { private redirectTo: RouteName = 'auth.login'; diff --git a/app/lib/tuyau.ts b/app/lib/tuyau.ts new file mode 100644 index 0000000..6169098 --- /dev/null +++ b/app/lib/tuyau.ts @@ -0,0 +1,22 @@ +import { api } from '#adonisjs/api'; +import { QueryParams } from '#types/query_params'; +import { RouteName } from '#types/tuyau'; + +export const getRoute = (routeName: RouteName, options?: QueryParams) => { + const current = api.routes.find((route) => route.name === routeName); + if (!current) { + throw new Error(`Route ${routeName} not found`); + } + + if (options?.qs) { + const searchParams = new URLSearchParams(options?.qs); + return { ...current, path: `${current.path}?${searchParams.toString()}` }; + } + + return current; +}; + +export function getPath(routeName: RouteName, options?: QueryParams) { + const current = getRoute(routeName, options); + return current.path; +} diff --git a/app/middleware/auth_middleware.ts b/app/middleware/auth_middleware.ts index afb149e..efaff50 100644 --- a/app/middleware/auth_middleware.ts +++ b/app/middleware/auth_middleware.ts @@ -1,7 +1,7 @@ +import { getPath } from '#lib/tuyau'; import type { Authenticators } from '@adonisjs/auth/types'; import type { HttpContext } from '@adonisjs/core/http'; import type { NextFn } from '@adonisjs/core/types/http'; -import { route } from '@izzyjs/route/client'; /** * Auth middleware is used authenticate HTTP requests and deny @@ -11,7 +11,7 @@ export default class AuthMiddleware { /** * The URL to redirect to, when authentication fails */ - redirectTo = route('auth.login').url; + redirectTo = getPath('auth.login'); async handle( ctx: HttpContext, diff --git a/app/types/query_params.ts b/app/types/query_params.ts new file mode 100644 index 0000000..30c01a8 --- /dev/null +++ b/app/types/query_params.ts @@ -0,0 +1 @@ +export type QueryParams = { qs?: Record }; diff --git a/app/types/tuyau.ts b/app/types/tuyau.ts new file mode 100644 index 0000000..d7622e4 --- /dev/null +++ b/app/types/tuyau.ts @@ -0,0 +1,4 @@ +import { api } from '#adonisjs/api'; + +export type RouteName = (typeof api)['routes'][number]['name']; +export type RouteParams = (typeof api)['routes'][number]['params']; diff --git a/app/validators/search_term.ts b/app/validators/search_term.ts new file mode 100644 index 0000000..d6da8b9 --- /dev/null +++ b/app/validators/search_term.ts @@ -0,0 +1,7 @@ +import vine from '@vinejs/vine'; + +export const searchTermValidator = vine.compile( + vine.object({ + searchTerm: vine.string().trim().minLength(1).maxLength(255), + }) +); diff --git a/config/tuyau.ts b/config/tuyau.ts new file mode 100644 index 0000000..4b272c9 --- /dev/null +++ b/config/tuyau.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@tuyau/core' + +const tuyauConfig = defineConfig({ + codegen: { + /** + * Filters the definitions and named routes to be generated + */ + // definitions: { + // only: [], + // } + // routes: { + // only: [], + // } + } +}) + +export default tuyauConfig \ No newline at end of file diff --git a/inertia/components/common/navigation/back_to_dashboard.tsx b/inertia/components/common/navigation/back_to_dashboard.tsx index e88d5cd..f3b36bf 100644 --- a/inertia/components/common/navigation/back_to_dashboard.tsx +++ b/inertia/components/common/navigation/back_to_dashboard.tsx @@ -1,16 +1,14 @@ import { router } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { ReactNode } from 'react'; import useSearchParam from '~/hooks/use_search_param'; import useShortcut from '~/hooks/use_shortcut'; -import { appendCollectionId } from '~/lib/navigation'; +import { routeWithCollectionId } from '~/lib/navigation'; export default function BackToDashboard({ children }: { children: ReactNode }) { const collectionId = Number(useSearchParam('collectionId')); useShortcut( 'ESCAPE_KEY', - () => - router.visit(appendCollectionId(route('dashboard').url, collectionId)), + () => router.visit(routeWithCollectionId('dashboard', collectionId)), { disableGlobalCheck: true } ); return <>{children}; diff --git a/inertia/components/dashboard/collection/header/collection_controls.tsx b/inertia/components/dashboard/collection/header/collection_controls.tsx index b73cfd7..d9bcc00 100644 --- a/inertia/components/dashboard/collection/header/collection_controls.tsx +++ b/inertia/components/dashboard/collection/header/collection_controls.tsx @@ -1,4 +1,3 @@ -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import { BsThreeDotsVertical } from 'react-icons/bs'; import { GoPencil } from 'react-icons/go'; @@ -6,7 +5,7 @@ import { IoIosAddCircleOutline } from 'react-icons/io'; import { IoTrashOutline } from 'react-icons/io5'; import Dropdown from '~/components/common/dropdown/dropdown'; import { DropdownItemLink } from '~/components/common/dropdown/dropdown_item'; -import { appendCollectionId } from '~/lib/navigation'; +import { getPath, routeWithCollectionId } from '~/lib/navigation'; import { Collection } from '~/types/app'; export default function CollectionControls({ @@ -17,22 +16,16 @@ export default function CollectionControls({ const { t } = useTranslation('common'); return ( } svgSize={18}> - + {t('link.create')} {t('collection.edit')} {t('collection.delete')} diff --git a/inertia/components/dashboard/collection/list/collection_item.tsx b/inertia/components/dashboard/collection/list/collection_item.tsx index 67913a0..f81639a 100644 --- a/inertia/components/dashboard/collection/list/collection_item.tsx +++ b/inertia/components/dashboard/collection/list/collection_item.tsx @@ -1,12 +1,11 @@ import styled from '@emotion/styled'; import { Link } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { useEffect, useRef } from 'react'; import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai'; import TextEllipsis from '~/components/common/text_ellipsis'; import { Item } from '~/components/dashboard/side_nav/nav_item'; import useActiveCollection from '~/hooks/use_active_collection'; -import { appendCollectionId } from '~/lib/navigation'; +import { routeWithCollectionId } from '~/lib/navigation'; import { CollectionWithLinks } from '~/types/app'; const CollectionItemStyle = styled(Item, { @@ -43,7 +42,7 @@ export default function CollectionItem({ return ( diff --git a/inertia/components/dashboard/dashboard_provider.tsx b/inertia/components/dashboard/dashboard_provider.tsx index 86e41ee..6525e72 100644 --- a/inertia/components/dashboard/dashboard_provider.tsx +++ b/inertia/components/dashboard/dashboard_provider.tsx @@ -1,12 +1,11 @@ import { router } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { ReactNode, useMemo, useState } from 'react'; import { ActiveCollectionContext } from '~/contexts/active_collection_context'; import CollectionsContext from '~/contexts/collections_context'; import FavoritesContext from '~/contexts/favorites_context'; import GlobalHotkeysContext from '~/contexts/global_hotkeys_context'; import useShortcut from '~/hooks/use_shortcut'; -import { appendCollectionId } from '~/lib/navigation'; +import { routeWithCollectionId } from '~/lib/navigation'; import { CollectionWithLinks, LinkWithCollection } from '~/types/app'; export default function DashboardProviders( @@ -28,7 +27,7 @@ export default function DashboardProviders( const handleChangeCollection = (collection: CollectionWithLinks) => { setActiveCollection(collection); - router.visit(appendCollectionId(route('dashboard').url, collection.id)); + router.visit(routeWithCollectionId('dashboard', collection.id)); }; // TODO: compute this in controller @@ -67,7 +66,7 @@ export default function DashboardProviders( 'OPEN_CREATE_LINK_KEY', () => router.visit( - appendCollectionId(route('link.create-form').url, activeCollection?.id) + routeWithCollectionId('link.create-form', activeCollection?.id) ), { enabled: globalHotkeysEnabled, @@ -75,7 +74,7 @@ export default function DashboardProviders( ); useShortcut( 'OPEN_CREATE_COLLECTION_KEY', - () => router.visit(route('collection.create-form').url), + () => router.visit('collection.create-form'), { enabled: globalHotkeysEnabled, } diff --git a/inertia/components/dashboard/link/link_controls.tsx b/inertia/components/dashboard/link/link_controls.tsx index 66fa209..c3b953c 100644 --- a/inertia/components/dashboard/link/link_controls.tsx +++ b/inertia/components/dashboard/link/link_controls.tsx @@ -1,5 +1,4 @@ import { useTheme } from '@emotion/react'; -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import { BsThreeDotsVertical } from 'react-icons/bs'; import { GoPencil } from 'react-icons/go'; @@ -7,7 +6,7 @@ import { IoTrashOutline } from 'react-icons/io5'; import Dropdown from '~/components/common/dropdown/dropdown'; import { DropdownItemLink } from '~/components/common/dropdown/dropdown_item'; import FavoriteDropdownItem from '~/components/dashboard/side_nav/favorite/favorite_dropdown_item'; -import { appendLinkId } from '~/lib/navigation'; +import { routeWithLinkId } from '~/lib/navigation'; import { Link } from '~/types/app'; export default function LinkControls({ link }: { link: Link }) { @@ -21,13 +20,11 @@ export default function LinkControls({ link }: { link: Link }) { svgSize={18} > - + {t('link.edit')} {t('link.delete')} diff --git a/inertia/components/dashboard/link/no_item.tsx b/inertia/components/dashboard/link/no_item.tsx index 31cd2bd..8368b7a 100644 --- a/inertia/components/dashboard/link/no_item.tsx +++ b/inertia/components/dashboard/link/no_item.tsx @@ -1,9 +1,8 @@ import styled from '@emotion/styled'; import { Link } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import useActiveCollection from '~/hooks/use_active_collection'; -import { appendCollectionId } from '~/lib/navigation'; +import { routeWithCollectionId } from '~/lib/navigation'; import { fadeIn } from '~/styles/keyframes'; const NoCollectionStyle = styled.div({ @@ -30,9 +29,7 @@ export function NoCollection() { return ( {t('select-collection')} - - {t('or-create-one')} - + {t('or-create-one')} ); } @@ -54,10 +51,7 @@ export function NoLink() { }} /> {t('link.create')} diff --git a/inertia/components/dashboard/search/search_modal.tsx b/inertia/components/dashboard/search/search_modal.tsx index c90956e..a2d5bee 100644 --- a/inertia/components/dashboard/search/search_modal.tsx +++ b/inertia/components/dashboard/search/search_modal.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import { route } from '@izzyjs/route/client'; import { FormEvent, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IoIosSearch } from 'react-icons/io'; @@ -11,7 +10,7 @@ import useActiveCollection from '~/hooks/use_active_collection'; import useCollections from '~/hooks/use_collections'; import useToggle from '~/hooks/use_modal'; import useShortcut from '~/hooks/use_shortcut'; -import { makeRequest } from '~/lib/request'; +import { tuyau, tuyauAbortController } from '~/lib/tuyau'; import { SearchResult } from '~/types/search'; const SearchInput = styled.input(({ theme }) => ({ @@ -78,18 +77,14 @@ function SearchModal({ openItem: OpenItem }: SearchModalProps) { return setResults([]); } - const controller = new AbortController(); - const { url, method } = route('search', { qs: { term: searchTerm } }); - makeRequest({ - method, - url, - controller, - }).then(({ results: _results }) => { - setResults(_results); - setSelectedItem(_results?.[0]); + tuyau.search.$get({ query: { searchTerm } }).then(({ error, data }) => { + if (error?.status === 404) return setResults([]); + const results = data?.results || []; + setResults(results); + setSelectedItem(results?.[0]); }); - return () => controller.abort(); + return () => tuyauAbortController.abort(); }, [searchTerm]); return ( diff --git a/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx b/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx index 5788d3f..5b563c5 100644 --- a/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx +++ b/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import { BsThreeDotsVertical } from 'react-icons/bs'; import { FaRegEye } from 'react-icons/fa'; @@ -12,7 +11,7 @@ import TextEllipsis from '~/components/common/text_ellipsis'; import LinkFavicon from '~/components/dashboard/link/link_favicon'; import FavoriteDropdownItem from '~/components/dashboard/side_nav/favorite/favorite_dropdown_item'; import { ItemExternalLink } from '~/components/dashboard/side_nav/nav_item'; -import { appendCollectionId, appendLinkId } from '~/lib/navigation'; +import { routeWithCollectionId, routeWithLinkId } from '~/lib/navigation'; import { LinkWithCollection } from '~/types/app'; const FavoriteItemStyle = styled(ItemExternalLink)(({ theme }) => ({ @@ -47,18 +46,16 @@ export default function FavoriteItem({ link }: { link: LinkWithCollection }) { svgSize={18} > {t('go-to-collection')} - + {t('link.edit')} {t('link.delete')} diff --git a/inertia/components/dashboard/side_nav/side_navigation.tsx b/inertia/components/dashboard/side_nav/side_navigation.tsx index 330159e..929a7c1 100644 --- a/inertia/components/dashboard/side_nav/side_navigation.tsx +++ b/inertia/components/dashboard/side_nav/side_navigation.tsx @@ -1,5 +1,4 @@ import styled from '@emotion/styled'; -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import { AiOutlineFolderAdd } from 'react-icons/ai'; import { IoAdd, IoShieldHalfSharp } from 'react-icons/io5'; @@ -11,7 +10,7 @@ import ModalSettings from '~/components/settings/settings_modal'; import useActiveCollection from '~/hooks/use_active_collection'; import useUser from '~/hooks/use_user'; import { rgba } from '~/lib/color'; -import { appendCollectionId } from '~/lib/navigation'; +import { getPath, routeWithCollectionId } from '~/lib/navigation'; const SideMenu = styled.nav(({ theme }) => ({ height: '100%', @@ -56,21 +55,18 @@ export default function SideNavigation() {
{user!.isAdmin && ( - + {t('admin')} )} {t('link.create')} - + {t('collection.create')}
diff --git a/inertia/components/footer/footer.tsx b/inertia/components/footer/footer.tsx index 48ed245..22ed620 100644 --- a/inertia/components/footer/footer.tsx +++ b/inertia/components/footer/footer.tsx @@ -1,9 +1,9 @@ import PATHS from '#constants/paths'; import styled from '@emotion/styled'; import { Link } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import ExternalLink from '~/components/common/external_link'; +import { getPath } from '~/lib/navigation'; import packageJson from '../../../package.json'; const FooterStyle = styled.footer(({ theme }) => ({ @@ -22,9 +22,9 @@ export default function Footer({ className }: { className?: string }) { return (
- {t('privacy')} + {t('privacy')} {' • '} - {t('terms')} + {t('terms')} {' • '} Extension
diff --git a/inertia/components/home/hero_header.tsx b/inertia/components/home/hero_header.tsx index 1334faf..a190e63 100644 --- a/inertia/components/home/hero_header.tsx +++ b/inertia/components/home/hero_header.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; import { Link } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import Quotes from '~/components/quotes'; +import { getPath } from '~/lib/navigation'; const HeroStyle = styled.header(({ theme }) => ({ height: '250px', @@ -46,9 +46,7 @@ export default function HeroHeader() { {t('about:hero.title')} {t('common:slogan')} - - {t('about:hero.cta')} - + {t('about:hero.cta')} ); } diff --git a/inertia/components/layouts/form_layout.tsx b/inertia/components/layouts/form_layout.tsx index 26f2f3a..db49041 100644 --- a/inertia/components/layouts/form_layout.tsx +++ b/inertia/components/layouts/form_layout.tsx @@ -1,13 +1,12 @@ import styled from '@emotion/styled'; import { Head, Link } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { FormEvent, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import Button from '~/components/common/form/_button'; import Form from '~/components/common/form/_form'; import TransitionLayout from '~/components/layouts/_transition_layout'; import i18n from '~/i18n'; -import { appendCollectionId } from '~/lib/navigation'; +import { routeWithCollectionId } from '~/lib/navigation'; import BaseLayout from './_base_layout'; const FormLayoutStyle = styled(TransitionLayout)(({ theme }) => ({ @@ -59,7 +58,7 @@ export default function FormLayout({ {!disableHomeLink && ( - + {t('back-home')} )} diff --git a/inertia/components/navbar/navbar.tsx b/inertia/components/navbar/navbar.tsx index 087067d..34e472b 100644 --- a/inertia/components/navbar/navbar.tsx +++ b/inertia/components/navbar/navbar.tsx @@ -1,7 +1,6 @@ import PATHS from '#constants/paths'; import styled from '@emotion/styled'; import { Link } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { useTranslation } from 'react-i18next'; import { IoIosLogOut } from 'react-icons/io'; import Dropdown from '~/components/common/dropdown/dropdown'; @@ -14,6 +13,7 @@ import RoundedImage from '~/components/common/rounded_image'; import UnstyledList from '~/components/common/unstyled/unstyled_list'; import ModalSettings from '~/components/settings/settings_modal'; import useUser from '~/hooks/use_user'; +import { getPath } from '~/lib/navigation'; type NavbarListDirection = { right?: boolean; @@ -66,7 +66,7 @@ export default function Navbar() {