From c23964a46db9fe3bd476013feff66970f24dec24 Mon Sep 17 00:00:00 2001 From: zarev <60230221+zarevskaya@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:37:46 +0200 Subject: [PATCH 01/33] Create common.json In french, if you want it ;) --- public/locales/fr/common.json | 374 ++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 public/locales/fr/common.json diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json new file mode 100644 index 0000000..c9a66f9 --- /dev/null +++ b/public/locales/fr/common.json @@ -0,0 +1,374 @@ +{ + "user_administration": "Administration des utilisateurs", + "search_users": "Rechercher des utilisateurs", + "no_users_found": "Aucun utilisateur trouvé.", + "no_user_found_in_search": "Aucun utilisateur trouvé avec la requête de recherche donnée.", + "username": "Nom d'utilisateur", + "email": "E-mail", + "subscribed": "Abonné(e)", + "created_at": "Created At", + "not_available": "S/O", + "check_your_email": "Veuillez vérifier votre adresse e-mail", + "authenticating": "Authentification...", + "verification_email_sent": "Envoi de l'email de vérification.", + "verification_email_sent_desc": "Un lien de connexion a été envoyé à votre adresse électronique. Si vous ne voyez pas l'e-mail, vérifiez votre dossier de courrier indésirable.", + "resend_email": "Renvoyez l'e-mail", + "invalid_credentials": "Informations d'identification invalides.", + "fill_all_fields": "Veuillez remplir tous les champs.", + "enter_credentials": "Entrez vos données d'identification", + "username_or_email": "Nom d'utilisateur ou e-mail", + "password": "Mot de passe", + "confirm_password": "Confirmer le mot de passe", + "forgot_password": "Mot de passe oublié ?", + "login": "Se connecter", + "or_continue_with": "Ou continuer avec", + "new_here": "Vous êtes nouveau ?", + "sign_up": "S'inscrire", + "sign_in_to_your_account": "Connectez-vous à votre compte", + "dashboard_desc": "Un bref aperçu de vos données", + "link": "Lien", + "links": "Liens", + "collection": "Collection", + "collections": "Collections", + "tag": "Étiquette", + "tags": "Étiquettes", + "recent": "Récent", + "recent_links_desc": "Liens récemment ajoutés", + "view_all": "Tout voir", + "view_added_links_here": "Consultez vos liens récemment ajoutés ici !", + "view_added_links_here_desc": "Cette section affichera vos derniers liens ajoutés dans toutes les collections auxquelles vous avez accès.", + "add_link": "Ajouter un nouveau lien", + "import_links": "Importer des liens", + "from_linkwarden": "De Linkwarden", + "from_html": "À partir d'un fichier HTML de signets", + "from_wallabag": "À partir de Wallabag (fichier JSON)", + "pinned": "Épinglé", + "pinned_links_desc": "Vos liens épinglés", + "pin_favorite_links_here": "Épinglez vos liens favoris ici !", + "pin_favorite_links_here_desc": "Vous pouvez épingler vos liens préférés en cliquant sur les trois points sur chaque lien et en cliquant sur Épingler au tableau de bord.", + "sending_password_link": "Envoi du lien de récupération du mot de passe...", + "password_email_prompt": "Entrez votre e-mail afin que nous puissions vous envoyer un lien pour créer un nouveau mot de passe.", + "send_reset_link": "Envoyer le lien de réinitialisation", + "reset_email_sent_desc": "Vérifiez votre e-mail pour trouver un lien permettant de réinitialiser votre mot de passe. S'il n'apparaît pas au bout de quelques minutes, vérifiez votre dossier spam.", + "back_to_login": "Retour à la page de connexion", + "email_sent": "Email envoyé!", + "passwords_mismatch": "Les mots de passe ne correspondent pas.", + "password_too_short": "Les mots de passe doivent comporter au moins 8 caractères.", + "creating_account": "Création d'un compte...", + "account_created": "Compte créé!", + "trial_offer_desc": "Débloquez {{count}} jours de service Premium sans frais !", + "register_desc": "Créer un nouveau compte", + "registration_disabled_desc": "L'inscription est désactivée pour cette instance, veuillez contacter l'administrateur en cas de problème.", + "enter_details": "Entrez vos coordonnées", + "display_name": "Nom affiché", + "sign_up_agreement": "En vous inscrivant, vous acceptez nos <0>Conditions d'utilisation et notre <1>Politique de confidentialité.", + "need_help": "Besoin d'aide?", + "get_in_touch": "Prenez contact avec nous", + "already_registered": "Vous avez déjà un compte?", + "deleting_selections": "Suppression des sélections...", + "links_deleted": "{{count}} Liens supprimés.", + "link_deleted": "1 Lien supprimé.", + "links_selected": "{{count}} Liens sélectionnés", + "link_selected": "1 Lien sélectionné", + "nothing_selected": "Rien de sélectionné", + "edit": "Modifier", + "delete": "Supprimer", + "nothing_found": "Rien n'a été trouvé.", + "redirecting_to_stripe": "Redirection vers Stripe...", + "subscribe_title": "Abonnez-vous à Linkwarden!", + "subscribe_desc": "Vous serez redirigé vers Stripe, n'hésitez pas à nous contacter à <0>support@linkwarden.app en cas de problème.", + "monthly": "Mensuel", + "yearly": "Annuel", + "discount_percent": "{{percent}}% de réduction", + "billed_monthly": "Facturé mensuellement", + "billed_yearly": "Facturé annuellement", + "total": "Total", + "total_annual_desc": "{{count}}-jour d'essai gratuit, puis ${{annualPrice}} annuellement", + "total_monthly_desc": "{{count}}-jour d'essai gratuit, puis ${{monthlyPrice}} par mois", + "plus_tax": "+ TVA si applicable", + "complete_subscription": "Abonnement complet", + "sign_out": "Se déconnecter", + "access_tokens": "Jetons d'accès", + "access_tokens_description": "Les jetons d'accès peuvent être utilisés pour accéder à Linkwarden à partir d'autres applications et services sans divulguer votre nom d'utilisateur et votre mot de passe.", + "new_token": "Nouveau jeton d'accès", + "name": "Nom", + "created_success": "Créé!", + "created": "Créé", + "expires": "Expire", + "accountSettings": "Paramètres du compte", + "language": "Langue", + "profile_photo": "Photo de profil", + "upload_new_photo": "Envoyer une nouvelle photo...", + "remove_photo": "Supprimer la photo", + "make_profile_private": "Rendre le profil privé", + "profile_privacy_info": "Cela limitera le nombre de personnes pouvant vous trouver et vous ajouter à de nouvelles collections..", + "whitelisted_users": "Utilisateurs sur liste blanche", + "whitelisted_users_info": "Veuillez fournir le nom d'utilisateur des utilisateurs auxquels vous souhaitez accorder de la visibilité à votre profil. Séparé par une virgule.", + "whitelisted_users_placeholder": "Votre profil est actuellement caché à tout le monde...", + "save_changes": "Sauvegarder les modifications", + "import_export": "Importer & Exporter", + "import_data": "Importez vos données depuis d'autres plateformes.", + "download_data": "Téléchargez vos données instantanément.", + "export_data": "Exporter des données", + "delete_account": "Supprimer le compte", + "delete_account_warning": "Cela supprimera définitivement TOUS les liens, collections, balises et données archivées que vous possédez.", + "cancel_subscription_notice": "Cela annulera également votre abonnement.", + "account_deletion_page": "Page de suppression de compte", + "applying_settings": "Application des paramètres...", + "settings_applied": "Paramètres appliqués !", + "email_change_request": "Demande de modification par e-mail envoyée. Veuillez vérifier la nouvelle adresse e-mail.", + "image_upload_size_error": "Veuillez sélectionner un fichier PNG ou JPEG de moins de 1 Mo.", + "image_upload_format_error": "Format de fichier invalide.", + "importing_bookmarks": "Importer des favoris...", + "import_success": "Importation des signets ! Rechargement de la page...", + "more_coming_soon": "Plus à venir bientôt!", + "billing_settings": "Paramètres de facturation", + "manage_subscription_intro": "Pour gérer/annuler votre abonnement, visitez le", + "billing_portal": "Portail de facturation", + "help_contact_intro": "Si vous avez encore besoin d'aide ou rencontrez des problèmes, n'hésitez pas à nous contacter à l'adresse suivante :", + "fill_required_fields": "Veuillez remplir les champs requis.", + "deleting_message": "Tout supprimer, veuillez patienter...", + "delete_warning": "Cela supprimera définitivement tous les liens, collections, balises et données archivées que vous possédez. Cela vous déconnectera également. Cette action est irréversible !", + "optional": "Facultatif", + "feedback_help": "(mais ça nous aide vraiment à nous améliorer !)", + "reason_for_cancellation": "Raison pour l'annulation", + "please_specify": "Veuillez préciser", + "customer_service": "Service client", + "low_quality": "Qualité médiocre", + "missing_features": "Fonctionnalités manquantes", + "switched_service": "Service commuté", + "too_complex": "Trop compliqué", + "too_expensive": "Trop cher", + "unused": "Inutilisé", + "other": "Autre", + "more_information": "Plus d'informations (plus il y a de détails, plus cela serait utile)", + "feedback_placeholder": "Par exemple: J'avais besoin d'une fonctionnalité qui...", + "delete_your_account": "Supprimer votre compte", + "change_password": "Changer le mot de passe", + "password_length_error": "Les mots de passe doivent comporter au moins 8 caractères.", + "applying_changes": "Appliquer...", + "password_change_instructions": "Pour modifier votre mot de passe, veuillez remplir ce qui suit. Votre mot de passe doit comporter au moins 8 caractères.", + "old_password": "Ancien mot de passe", + "new_password": "Nouveau mot de passe", + "preference": "Préférence", + "select_theme": "Sélectionner un thème", + "dark": "Sombre", + "light": "Clair", + "archive_settings": "Paramètres d'archivage", + "formats_to_archive": "Formats pour archiver/préserver les pages Web:", + "screenshot": "Capture d'écran", + "pdf": "PDF", + "archive_org_snapshot": "Instantané d'Archive.org", + "link_settings": "Paramètres du lien", + "prevent_duplicate_links": "Empêcher les liens en double", + "clicking_on_links_should": "Cliquer sur les liens devrait:", + "open_original_content": "Ouvrir le contenu original", + "open_pdf_if_available": "Ouvrir le PDF, si disponible", + "open_readable_if_available": "Ouvrir Readable, si disponible", + "open_screenshot_if_available": "Ouvrez la capture d'écran, si disponible", + "open_webpage_if_available": "Ouvrir la copie de la page Web, si disponible", + "tag_renamed": "L'étiquette a été renommée !", + "tag_deleted": "Étiquette supprimée!", + "rename_tag": "Renommer l'étiquette", + "delete_tag": "Supprimer l'étiquette", + "list_created_with_linkwarden": "Liste créée avec Linkwarden", + "by_author": "Par {{author}}.", + "by_author_and_other": "Par {{author}} et {{count}} autre.", + "by_author_and_others": "Par {{author}} et {{count}} autres.", + "search_count_link": "Recherche {{count}} Lien", + "search_count_links": "Recherche {{count}} Liens", + "collection_is_empty": "Cette collection est vide...", + "all_links": "Tous les liens", + "all_links_desc": "Liens de chaque collection", + "you_have_not_added_any_links": "Vous n'avez pas encore créé de liens", + "collections_you_own": "Collections que vous possédez", + "new_collection": "Nouvelle collection", + "other_collections": "Autres collections", + "other_collections_desc": "Collections partagées dont vous êtes membre", + "showing_count_results": "Afficher {{count}} résultats", + "showing_count_result": "Afficher {{count}} résultat", + "edit_collection_info": "Modifier les informations sur la collection", + "share_and_collaborate": "Partager et collaborer", + "view_team": "Voir l'équipe", + "team": "L'équipe", + "create_subcollection": "Créer une sous-collection", + "delete_collection": "Supprimer la collection", + "leave_collection": "Quitter la collection", + "email_verified_signing_out": "Courriel vérifié. Déconnexion...", + "invalid_token": "Jeton non valide.", + "sending_password_recovery_link": "Envoi du lien de récupération du mot de passe...", + "please_fill_all_fields": "Veuillez remplir tous les champs.", + "password_updated": "Mot de passe mis à jour!", + "reset_password": "Réinitialiser le mot de passe", + "enter_email_for_new_password": "Saisissez votre adresse électronique afin que nous puissions vous envoyer un lien pour créer un nouveau mot de passe.", + "update_password": "Mise à jour du mot de passe", + "password_successfully_updated": "Votre mot de passe a été mis à jour avec succès.", + "user_already_member": "L'utilisateur existe déjà.", + "you_are_already_collection_owner": "Vous êtes déjà le propriétaire de la collection.", + "date_newest_first": "Date (la plus récente en premier)", + "date_oldest_first": "Date (la plus ancienne en premier)", + "name_az": "Nom (A-Z)", + "name_za": "Nom (Z-A)", + "description_az": "Description (A-Z)", + "description_za": "Description (Z-A)", + "all_rights_reserved": "© {{date}} <0>Linkwarden. Tous droits réservés.", + "you_have_no_collections": "Vous n'avez pas de Collections...", + "you_have_no_tags": "Vous n’avez pas d’étiquettes...", + "cant_change_collection_you_dont_own": "Vous ne pouvez pas modifier une collection dont vous n'êtes pas propriétaire.", + "account": "Compte", + "billing": "Facturation", + "linkwarden_version": "Linkwarden {{version}}", + "help": "Aide", + "github": "GitHub", + "twitter": "Twitter", + "mastodon": "Mastodon", + "link_preservation_in_queue": "La préservation du lien est actuellement dans la file d'attente", + "check_back_later": "Revenez plus tard pour voir le résultat", + "there_are_more_formats": "Il y a plus de formats conservés dans la file d'attente", + "settings": "Paramètres", + "switch_to": "Basculer vers {{theme}}", + "logout": "Déconnexion", + "start_journey": "Commencez votre voyage en créant un nouveau lien!", + "create_new_link": "Créer un nouveau lien", + "new_link": "Nouveau lien", + "create_new": "Créer nouveau...", + "pwa_install_prompt": "Installez Linkwarden sur votre écran d'accueil pour un accès plus rapide et une expérience améliorée. <0>En savoir plus", + "full_content": "Contenu complet", + "slower": "Plus lent", + "new_version_announcement": "Voir ce qu'il y a de nouveau dans <0>Linkwarden {{version}}!", + "creating": "Création...", + "upload_file": "Envoyer fichier", + "file": "Ficher", + "file_types": "PDF, PNG, JPG (Jusqu'à {{size}} MB)", + "description": "Description", + "auto_generated": "Sera généré automatiquement si rien n'est fourni.", + "example_link": "Par exemple: Exemple de lien", + "hide": "Cacher", + "more": "Plus", + "options": "Options", + "description_placeholder": "Notes, réflexions, etc.", + "deleting": "Suppression...", + "token_revoked": "Jeton révoqué.", + "revoke_token": "Révoquer le jeton", + "revoke_confirmation": "Êtes-vous sûr de vouloir révoquer ce jeton d'accès ? Les applications ou services utilisant ce jeton ne pourront plus accéder à Linkwarden en l'utilisant.", + "revoke": "Révoquer", + "sending_request": "Envoi de la demande...", + "link_being_archived": "Le lien est en cours d'archivage...", + "preserved_formats": "Formats conservés", + "available_formats": "Les formats suivants sont disponibles pour ce lien:", + "readable": "Lisible", + "preservation_in_queue": "La préservation du lien est dans la file d'attente", + "view_latest_snapshot": "Voir le dernier instantané sur archive.org", + "refresh_preserved_formats": "Rafraîchir les formats conservés", + "this_deletes_current_preservations": "Cette opération supprime les conservations en cours", + "create_new_user": "Créer un nouvel utilisateur", + "placeholder_johnny": "Johnny", + "placeholder_email": "johnny@exemple.com", + "placeholder_john": "john", + "user_created": "Utilisateur créé!", + "fill_all_fields_error": "Veuillez remplir tous les champs.", + "password_change_note": "<0>Note: Veillez à informer l'utilisateur qu'il doit modifier son mot de passe..", + "create_user": "Créer un utilisateur", + "creating_token": "Création d'un jeton...", + "token_created": "Jeton créé!", + "access_token_created": "Jeton d'accès créé", + "token_creation_notice": "Votre nouveau jeton a été créé. Veuillez le copier et le conserver en lieu sûr. Vous ne pourrez plus le voir.", + "copied_to_clipboard": "Copié dans le presse-papiers !", + "copy_to_clipboard": "Copier dans le presse-papiers", + "create_access_token": "Créer un jeton d'accès", + "expires_in": "Expire dans", + "token_name_placeholder": "Par exemple: Pour le raccourci iOS", + "create_token": "Créer un jeton d'accès", + "7_days": "7 jous", + "30_days": "30 jours", + "60_days": "60 jours", + "90_days": "90 jous", + "no_expiration": "Pas d'expiration", + "creating_link": "Création d'un lien...", + "link_created": "Lien créé!", + "link_name_placeholder": "Sera généré automatiquement s'il n'est pas renseigné.", + "link_url_placeholder": "Par exemple: http://exemple.com/", + "link_description_placeholder": "Notes, réflexions, etc.", + "more_options": "Plus d'options", + "hide_options": "Cacher les options", + "create_link": "Créer un lien", + "new_sub_collection": "Nouvelle sous-collection", + "for_collection": "Pour {{name}}", + "create_new_collection": "Créer une nouvelle collection", + "color": "Couleur", + "reset": "Réinitialiser", + "collection_name_placeholder": "Par exemple: Exemple de collection", + "collection_description_placeholder": "L'objectif de cette collection...", + "create_collection_button": "Créer une collection", + "password_change_warning": "Veuillez confirmer votre mot de passe avant de modifier votre adresse électronique.", + "stripe_update_note": " La mise à jour de ce champ modifiera également votre email de facturation sur Stripe.", + "sso_will_be_removed_warning": "Si vous changez d'adresse électronique, toutes les connexions SSO {{service}} existantes seront supprimées.", + "old_email": "Ancien e-mail", + "new_email": "Nouvel e-mail", + "confirm": "Confirmer", + "edit_link": "Modifier le lien", + "updating": "Mise à jour...", + "updated": "Mis à jour!", + "placeholder_example_link": "Par exemple: Lien d'exemple", + "make_collection_public": "Rendre la collection publique", + "make_collection_public_checkbox": "Faire de cette collection une collection publique", + "make_collection_public_desc": "Cela permettra à n'importe qui de consulter cette collection et ses utilisateurs.", + "sharable_link_guide": "Lien partageable (Cliquez pour copier)", + "copied": "Copié!", + "members": "Membres", + "members_username_placeholder": "Nom d'utilisateur (sans le '@')", + "owner": "Propriétaire", + "admin": "Administrateur", + "contributor": "Contributeur", + "viewer": "Visualiseur", + "viewer_desc": "Accès en lecture seule", + "contributor_desc": "Peut consulter et créer des liens", + "admin_desc": "Accès complet à tous les liens", + "remove_member": "Supprimer le membre", + "placeholder_example_collection": "Par exemple: Exemple de collection", + "placeholder_collection_purpose": "L'objectif de cette collection...", + "deleting_user": "Suppression...", + "user_deleted": "Utilisateur supprimé.", + "delete_user": "Supprimer l'utilisateur", + "confirm_user_deletion": "Êtes-vous sûr de vouloir supprimer cet utilisateur ?", + "irreversible_action_warning": "Cette action est irréversible !", + "delete_confirmation": "Supprimer, je sais ce que je fais", + "delete_link": "Supprimer le lien", + "deleted": "Supprimé.", + "link_deletion_confirmation_message": "Êtes-vous sûr de vouloir supprimer ce lien ?", + "warning": "Attention", + "irreversible_warning": "Cette action est irréversible !", + "shift_key_tip": "Maintenez la touche Majuscule enfoncée tout en cliquant sur « Supprimer » pour éviter cette confirmation à l'avenir.", + "deleting_collection": "Suppression...", + "collection_deleted": "Collection supprimée.", + "confirm_deletion_prompt": "Pour confirmer, tapez \"{{name}}\" dans le champ ci-dessous:", + "type_name_placeholder": "Tapez \"{{name}}\" ici.", + "deletion_warning": "La suppression de cette collection effacera définitivement tout son contenu et deviendra inaccessible à tous, y compris aux membres qui y ont déjà eu accès.", + "leave_prompt": "Cliquez sur le bouton ci-dessous pour quitter la collection actuelle.", + "leave": "Quitter", + "edit_links": "Éditer {{count}} liens", + "move_to_collection": "Déplacer vers la collection", + "add_tags": "Ajouter des étiquettes", + "remove_previous_tags": "Supprimer les étiquettes précédentes", + "delete_links": "Supprimer {{count}} liens", + "links_deletion_confirmation_message": "Êtes-vous sûr de vouloir supprimer {{count}} liens ? ", + "warning_irreversible": "Avertissement : Cette action est irréversible !", + "shift_key_instruction": "Maintenez la touche « Shift » enfoncée tout en cliquant sur « Supprimer » pour éviter cette confirmation à l'avenir.", + "link_selection_error": "Vous n'avez pas l'autorisation de modifier ou de supprimer cet élément.", + "no_description": "Aucune description n'est fournie.", + "applying": "Appliquer...", + "unpin": "Épingler", + "pin_to_dashboard": "Épingler au tableau de bord", + "show_link_details": "Afficher les détails du lien", + "hide_link_details": "Cacher les détails du lien", + "link_pinned": "Lien épinglé !", + "link_unpinned": "Lien désépinglé !", + "webpage": "Page web", + "server_administration": "Administration du serveur", + "all_collections": "Toutes les collections", + "dashboard": "Tableau de bord", + "demo_title": "Démonstration uniquement", + "demo_desc": "Il s'agit d'une instance de démonstration de Linkwarden et les téléchargements sont désactivés.", + "demo_desc_2": "Si vous souhaitez tester la version complète, vous pouvez vous inscrire pour un essai gratuit à l'adresse suivante:", + "demo_button": "Se connecter en tant qu'utilisateur de démonstration" +} From abc93f1bf9d034e31c5656f464a5fa4fa6a67597 Mon Sep 17 00:00:00 2001 From: zarev <60230221+zarevskaya@users.noreply.github.com> Date: Sat, 20 Jul 2024 09:55:51 +0200 Subject: [PATCH 02/33] Update common.json Correction --- public/locales/fr/common.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index c9a66f9..eb789b0 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -6,7 +6,7 @@ "username": "Nom d'utilisateur", "email": "E-mail", "subscribed": "Abonné(e)", - "created_at": "Created At", + "created_at": "Créé le", "not_available": "S/O", "check_your_email": "Veuillez vérifier votre adresse e-mail", "authenticating": "Authentification...", From 1b9dafbe476cd4c6c9e1a81583029d7daf44f81c Mon Sep 17 00:00:00 2001 From: Isaac Wise Date: Mon, 22 Jul 2024 17:39:38 -0500 Subject: [PATCH 03/33] Handle 400 error code when accesing a non public collection --- lib/client/getPublicCollectionData.ts | 2 ++ pages/public/collections/[id].tsx | 40 ++++++++++++++------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/client/getPublicCollectionData.ts b/lib/client/getPublicCollectionData.ts index 283733b..85deea6 100644 --- a/lib/client/getPublicCollectionData.ts +++ b/lib/client/getPublicCollectionData.ts @@ -9,6 +9,8 @@ const getPublicCollectionData = async ( ) => { const res = await fetch("/api/v1/public/collections/" + collectionId); + if (res.status === 400) return { response: "Collection not found.", status: 400 }; + const data = await res.json(); setData(data.response); diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 47e074f..efe0554 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -68,12 +68,15 @@ export default function PublicCollections() { searchByTags: searchFilter.tags, }); - const [collection, setCollection] = - useState(); + const [collection, setCollection] = useState(); useEffect(() => { if (router.query.id) { - getPublicCollectionData(Number(router.query.id), setCollection); + getPublicCollectionData(Number(router.query.id), setCollection).then((res) => { + if (res.status === 400) { + router.push("/dashboard"); + } + }) } }, [collections]); @@ -108,9 +111,8 @@ export default function PublicCollections() {
{collection ? ( @@ -181,20 +183,20 @@ export default function PublicCollections() {

{collection.members.length > 0 && - collection.members.length === 1 + collection.members.length === 1 ? t("by_author_and_other", { + author: collectionOwner.name, + count: collection.members.length, + }) + : collection.members.length > 0 && + collection.members.length !== 1 + ? t("by_author_and_others", { author: collectionOwner.name, count: collection.members.length, }) - : collection.members.length > 0 && - collection.members.length !== 1 - ? t("by_author_and_others", { - author: collectionOwner.name, - count: collection.members.length, - }) : t("by_author", { - author: collectionOwner.name, - })} + author: collectionOwner.name, + })}

@@ -218,11 +220,11 @@ export default function PublicCollections() { placeholder={ collection._count?.links === 1 ? t("search_count_link", { - count: collection._count?.links, - }) + count: collection._count?.links, + }) : t("search_count_links", { - count: collection._count?.links, - }) + count: collection._count?.links, + }) } /> From 7d43ed52a4fe0dc32e1cfe999ebe693aac6cd1cc Mon Sep 17 00:00:00 2001 From: Isaac Wise Date: Mon, 22 Jul 2024 17:50:24 -0500 Subject: [PATCH 04/33] format --- .../migration/importFromHTMLFile.ts | 4 +- lib/client/getPublicCollectionData.ts | 3 +- next-i18next.config.js | 2 +- pages/public/collections/[id].tsx | 44 ++++++++++--------- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/lib/api/controllers/migration/importFromHTMLFile.ts b/lib/api/controllers/migration/importFromHTMLFile.ts index b2b58a1..59a2787 100644 --- a/lib/api/controllers/migration/importFromHTMLFile.ts +++ b/lib/api/controllers/migration/importFromHTMLFile.ts @@ -63,7 +63,8 @@ async function processBookmarks( ) as Element; if (collectionName) { - const collectionNameContent = (collectionName.children[0] as TextNode)?.content; + const collectionNameContent = (collectionName.children[0] as TextNode) + ?.content; if (collectionNameContent) { collectionId = await createCollection( userId, @@ -274,4 +275,3 @@ function processNodes(nodes: Node[]) { nodes.forEach(findAndProcessDL); return nodes; } - diff --git a/lib/client/getPublicCollectionData.ts b/lib/client/getPublicCollectionData.ts index 85deea6..a8a74a3 100644 --- a/lib/client/getPublicCollectionData.ts +++ b/lib/client/getPublicCollectionData.ts @@ -9,7 +9,8 @@ const getPublicCollectionData = async ( ) => { const res = await fetch("/api/v1/public/collections/" + collectionId); - if (res.status === 400) return { response: "Collection not found.", status: 400 }; + if (res.status === 400) + return { response: "Collection not found.", status: 400 }; const data = await res.json(); diff --git a/next-i18next.config.js b/next-i18next.config.js index 06c3d23..b78b3b8 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -2,7 +2,7 @@ module.exports = { i18n: { defaultLocale: "en", - locales: ["en","it"], + locales: ["en", "it"], }, reloadOnPrerender: process.env.NODE_ENV === "development", }; diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index efe0554..7dba911 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -68,15 +68,18 @@ export default function PublicCollections() { searchByTags: searchFilter.tags, }); - const [collection, setCollection] = useState(); + const [collection, setCollection] = + useState(); useEffect(() => { if (router.query.id) { - getPublicCollectionData(Number(router.query.id), setCollection).then((res) => { - if (res.status === 400) { - router.push("/dashboard"); + getPublicCollectionData(Number(router.query.id), setCollection).then( + (res) => { + if (res.status === 400) { + router.push("/dashboard"); + } } - }) + ); } }, [collections]); @@ -111,8 +114,9 @@ export default function PublicCollections() {
{collection ? ( @@ -183,20 +187,20 @@ export default function PublicCollections() {

{collection.members.length > 0 && - collection.members.length === 1 + collection.members.length === 1 ? t("by_author_and_other", { - author: collectionOwner.name, - count: collection.members.length, - }) - : collection.members.length > 0 && - collection.members.length !== 1 - ? t("by_author_and_others", { author: collectionOwner.name, count: collection.members.length, }) + : collection.members.length > 0 && + collection.members.length !== 1 + ? t("by_author_and_others", { + author: collectionOwner.name, + count: collection.members.length, + }) : t("by_author", { - author: collectionOwner.name, - })} + author: collectionOwner.name, + })}

@@ -220,11 +224,11 @@ export default function PublicCollections() { placeholder={ collection._count?.links === 1 ? t("search_count_link", { - count: collection._count?.links, - }) + count: collection._count?.links, + }) : t("search_count_links", { - count: collection._count?.links, - }) + count: collection._count?.links, + }) } /> From 05c5bdf63cf7b1647f8716e66e0fee18c91d5e40 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 30 Jul 2024 14:57:09 -0400 Subject: [PATCH 05/33] refactor collections store --- components/CollectionListing.tsx | 8 +- .../InputSelect/CollectionSelection.tsx | 6 +- components/LinkViews/LinkCard.tsx | 6 +- components/LinkViews/LinkList.tsx | 6 +- components/LinkViews/LinkMasonry.tsx | 5 +- .../ModalContent/DeleteCollectionModal.tsx | 25 +- .../ModalContent/EditCollectionModal.tsx | 20 +- .../EditCollectionSharingModal.tsx | 21 +- .../ModalContent/NewCollectionModal.tsx | 21 +- components/ModalContent/NewLinkModal.tsx | 6 +- components/ModalContent/UploadFileModal.tsx | 5 +- components/ReadableView.tsx | 5 +- components/Sidebar.tsx | 6 +- components/ui/Loader.tsx | 272 ++++++++++++++++++ hooks/store/collections.tsx | 131 +++++++++ hooks/useCollectivePermissions.ts | 5 +- hooks/useInitialData.tsx | 5 +- hooks/usePermissions.tsx | 5 +- package.json | 2 + pages/_app.tsx | 140 ++++----- pages/collections/[id].tsx | 5 +- pages/collections/index.tsx | 5 +- pages/dashboard.tsx | 5 +- pages/public/collections/[id].tsx | 5 +- store/collections.ts | 94 ------ yarn.lock | 24 ++ 26 files changed, 585 insertions(+), 253 deletions(-) create mode 100644 components/ui/Loader.tsx create mode 100644 hooks/store/collections.tsx delete mode 100644 store/collections.ts diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index fbe2f7b..d3b433e 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -9,7 +9,6 @@ import Tree, { TreeSourcePosition, TreeDestinationPosition, } from "@atlaskit/tree"; -import useCollectionStore from "@/store/collections"; import { Collection } from "@prisma/client"; import Link from "next/link"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; @@ -17,6 +16,7 @@ import { useRouter } from "next/router"; import useAccountStore from "@/store/account"; import toast from "react-hot-toast"; import { useTranslation } from "next-i18next"; +import { useCollections, useUpdateCollection } from "@/hooks/store/collections"; interface ExtendedTreeItem extends TreeItem { data: Collection; @@ -24,7 +24,9 @@ interface ExtendedTreeItem extends TreeItem { const CollectionListing = () => { const { t } = useTranslation(); - const { collections, updateCollection } = useCollectionStore(); + const updateCollection = useUpdateCollection(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const { account, updateAccount } = useAccountStore(); const router = useRouter(); @@ -151,7 +153,7 @@ const CollectionListing = () => { const updatedCollectionOrder = [...account.collectionOrder]; if (source.parentId !== destination.parentId) { - await updateCollection({ + await updateCollection.mutateAsync({ ...movedCollection, parentId: destination.parentId && destination.parentId !== "root" diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 99b999e..8a98b9e 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -1,10 +1,10 @@ -import useCollectionStore from "@/store/collections"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { styles } from "./styles"; import { Options } from "./types"; import CreatableSelect from "react-select/creatable"; import Select from "react-select"; +import { useCollections } from "@/hooks/store/collections"; type Props = { onChange: any; @@ -24,7 +24,9 @@ export default function CollectionSelection({ showDefaultValue = true, creatable = true, }: Props) { - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); + const router = useRouter(); const [options, setOptions] = useState([]); diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx index 70f553c..20597b7 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkCard.tsx @@ -5,7 +5,6 @@ import { } from "@/types/global"; import { useEffect, useRef, useState } from "react"; import useLinkStore from "@/store/links"; -import useCollectionStore from "@/store/collections"; import unescapeString from "@/lib/client/unescapeString"; import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions"; import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate"; @@ -21,6 +20,7 @@ import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -34,7 +34,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); const viewMode = localStorage.getItem("viewMode") || "card"; - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); + const { account } = useAccountStore(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index 723c47e..936d857 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -4,7 +4,6 @@ import { } from "@/types/global"; import { useEffect, useState } from "react"; import useLinkStore from "@/store/links"; -import useCollectionStore from "@/store/collections"; import unescapeString from "@/lib/client/unescapeString"; import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions"; import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate"; @@ -17,6 +16,7 @@ import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -33,7 +33,9 @@ export default function LinkCardCompact({ }: Props) { const { t } = useTranslation(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); + const { account } = useAccountStore(); const { links, setSelectedLinks, selectedLinks } = useLinkStore(); diff --git a/components/LinkViews/LinkMasonry.tsx b/components/LinkViews/LinkMasonry.tsx index 022ac93..c3c2451 100644 --- a/components/LinkViews/LinkMasonry.tsx +++ b/components/LinkViews/LinkMasonry.tsx @@ -5,7 +5,6 @@ import { } from "@/types/global"; import { useEffect, useRef, useState } from "react"; import useLinkStore from "@/store/links"; -import useCollectionStore from "@/store/collections"; import unescapeString from "@/lib/client/unescapeString"; import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions"; import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate"; @@ -21,6 +20,7 @@ import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -33,7 +33,8 @@ type Props = { export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const { account } = useAccountStore(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); diff --git a/components/ModalContent/DeleteCollectionModal.tsx b/components/ModalContent/DeleteCollectionModal.tsx index 504d3a3..907f1c0 100644 --- a/components/ModalContent/DeleteCollectionModal.tsx +++ b/components/ModalContent/DeleteCollectionModal.tsx @@ -1,13 +1,12 @@ import React, { useEffect, useState } from "react"; import TextInput from "@/components/TextInput"; -import useCollectionStore from "@/store/collections"; -import toast from "react-hot-toast"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { useRouter } from "next/router"; import usePermissions from "@/hooks/usePermissions"; import Modal from "../Modal"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; +import { useDeleteCollection } from "@/hooks/store/collections"; type Props = { onClose: Function; @@ -22,7 +21,6 @@ export default function DeleteCollectionModal({ const [collection, setCollection] = useState(activeCollection); const [submitLoader, setSubmitLoader] = useState(false); - const { removeCollection } = useCollectionStore(); const router = useRouter(); const [inputField, setInputField] = useState(""); const permissions = usePermissions(collection.id as number); @@ -31,6 +29,8 @@ export default function DeleteCollectionModal({ setCollection(activeCollection); }, []); + const deleteCollection = useDeleteCollection(); + const submit = async () => { if (permissions === true && collection.name !== inputField) return; if (!submitLoader) { @@ -39,19 +39,12 @@ export default function DeleteCollectionModal({ setSubmitLoader(true); - const load = toast.loading(t("deleting_collection")); - - let response = await removeCollection(collection.id as number); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("deleted")); - onClose(); - router.push("/collections"); - } else { - toast.error(response.data as string); - } + deleteCollection.mutateAsync(collection.id as number, { + onSuccess: () => { + onClose(); + router.push("/collections"); + }, + }); setSubmitLoader(false); } diff --git a/components/ModalContent/EditCollectionModal.tsx b/components/ModalContent/EditCollectionModal.tsx index b2105f5..3009122 100644 --- a/components/ModalContent/EditCollectionModal.tsx +++ b/components/ModalContent/EditCollectionModal.tsx @@ -1,11 +1,10 @@ import React, { useState } from "react"; import TextInput from "@/components/TextInput"; -import useCollectionStore from "@/store/collections"; -import toast from "react-hot-toast"; import { HexColorPicker } from "react-colorful"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; +import { useUpdateCollection } from "@/hooks/store/collections"; type Props = { onClose: Function; @@ -21,7 +20,7 @@ export default function EditCollectionModal({ useState(activeCollection); const [submitLoader, setSubmitLoader] = useState(false); - const { updateCollection } = useCollectionStore(); + const updateCollection = useUpdateCollection(); const submit = async () => { if (!submitLoader) { @@ -30,16 +29,11 @@ export default function EditCollectionModal({ setSubmitLoader(true); - const load = toast.loading(t("updating_collection")); - - let response = await updateCollection(collection as any); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("updated")); - onClose(); - } else toast.error(response.data as string); + await updateCollection.mutateAsync(collection, { + onSuccess: () => { + onClose(); + }, + }); setSubmitLoader(false); } diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index 9a9f5fa..10831d7 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import TextInput from "@/components/TextInput"; -import useCollectionStore from "@/store/collections"; import toast from "react-hot-toast"; import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import getPublicUserData from "@/lib/client/getPublicUserData"; @@ -11,6 +10,7 @@ import addMemberToCollection from "@/lib/client/addMemberToCollection"; import Modal from "../Modal"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; +import { useUpdateCollection } from "@/hooks/store/collections"; type Props = { onClose: Function; @@ -27,7 +27,7 @@ export default function EditCollectionSharingModal({ useState(activeCollection); const [submitLoader, setSubmitLoader] = useState(false); - const { updateCollection } = useCollectionStore(); + const updateCollection = useUpdateCollection(); const submit = async () => { if (!submitLoader) { @@ -36,18 +36,11 @@ export default function EditCollectionSharingModal({ setSubmitLoader(true); - const load = toast.loading(t("updating")); - - let response; - - response = await updateCollection(collection as any); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("updated")); - onClose(); - } else toast.error(response.data as string); + await updateCollection.mutateAsync(collection, { + onSuccess: () => { + onClose(); + }, + }); setSubmitLoader(false); } diff --git a/components/ModalContent/NewCollectionModal.tsx b/components/ModalContent/NewCollectionModal.tsx index 980bb44..e1ab424 100644 --- a/components/ModalContent/NewCollectionModal.tsx +++ b/components/ModalContent/NewCollectionModal.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useState } from "react"; import TextInput from "@/components/TextInput"; -import useCollectionStore from "@/store/collections"; -import toast from "react-hot-toast"; import { HexColorPicker } from "react-colorful"; import { Collection } from "@prisma/client"; import Modal from "../Modal"; @@ -9,6 +7,7 @@ import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import useAccountStore from "@/store/account"; import { useSession } from "next-auth/react"; import { useTranslation } from "next-i18next"; +import { useCreateCollection } from "@/hooks/store/collections"; type Props = { onClose: Function; @@ -33,7 +32,8 @@ export default function NewCollectionModal({ onClose, parent }: Props) { }, []); const [submitLoader, setSubmitLoader] = useState(false); - const { addCollection } = useCollectionStore(); + + const createCollection = useCreateCollection(); const submit = async () => { if (submitLoader) return; @@ -41,18 +41,11 @@ export default function NewCollectionModal({ onClose, parent }: Props) { setSubmitLoader(true); - const load = toast.loading(t("creating")); - - let response = await addCollection(collection as any); - toast.dismiss(load); - - if (response.ok) { - toast.success(t("created_success")); - if (response.data) { - setAccount(data?.user.id as number); + await createCollection.mutateAsync(collection, { + onSuccess: () => { onClose(); - } - } else toast.error(response.data as string); + }, + }); setSubmitLoader(false); }; diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 0284687..605d03c 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useState } from "react"; -import { Toaster } from "react-hot-toast"; import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useCollectionStore from "@/store/collections"; import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { useSession } from "next-auth/react"; @@ -12,6 +10,7 @@ import { useRouter } from "next/router"; import toast from "react-hot-toast"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; type Props = { onClose: Function; @@ -44,7 +43,8 @@ export default function NewLinkModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 2480431..c5c3b5e 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -3,7 +3,6 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useCollectionStore from "@/store/collections"; import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags, @@ -14,6 +13,7 @@ import { useRouter } from "next/router"; import toast from "react-hot-toast"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; type Props = { onClose: Function; @@ -49,7 +49,8 @@ export default function UploadFileModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index b600236..e10461b 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -14,8 +14,8 @@ import Link from "next/link"; import { useRouter } from "next/router"; import React, { useEffect, useMemo, useState } from "react"; import LinkActions from "./LinkViews/LinkComponents/LinkActions"; -import useCollectionStore from "@/store/collections"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; type LinkContent = { title: string; @@ -46,7 +46,8 @@ export default function ReadableView({ link }: Props) { const router = useRouter(); const { getLink } = useLinkStore(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const collection = useMemo(() => { return collections.find( diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 11a0f62..20cfd0b 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -1,4 +1,3 @@ -import useCollectionStore from "@/store/collections"; import useTagStore from "@/store/tags"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -7,6 +6,7 @@ import { Disclosure, Transition } from "@headlessui/react"; import SidebarHighlightLink from "@/components/SidebarHighlightLink"; import CollectionListing from "@/components/CollectionListing"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; export default function Sidebar({ className }: { className?: string }) { const { t } = useTranslation(); @@ -22,7 +22,9 @@ export default function Sidebar({ className }: { className?: string }) { } ); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); + const { tags } = useTagStore(); const [active, setActive] = useState(""); diff --git a/components/ui/Loader.tsx b/components/ui/Loader.tsx new file mode 100644 index 0000000..ac5f570 --- /dev/null +++ b/components/ui/Loader.tsx @@ -0,0 +1,272 @@ +import React from "react"; + +type Props = { + className?: string; + color: string; + size: string; +}; + +const Loader = (props: Props) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default Loader; diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx new file mode 100644 index 0000000..46109dc --- /dev/null +++ b/hooks/store/collections.tsx @@ -0,0 +1,131 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; +import { useTranslation } from "next-i18next"; +import toast from "react-hot-toast"; + +const useCollections = () => { + return useQuery({ + queryKey: ["collections"], + queryFn: async (): Promise<{ + response: CollectionIncludingMembersAndLinkCount[]; + }> => { + const response = await fetch("/api/v1/collections"); + const data = await response.json(); + return data; + }, + }); +}; + +const useCreateCollection = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: any) => { + const load = toast.loading(t("creating")); + + const response = await fetch("/api/v1/collections", { + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + toast.dismiss(load); + + return response.json(); + }, + onSuccess: (data) => { + toast.success(t("created")); + return queryClient.setQueryData(["collections"], (oldData: any) => { + return { + response: [...oldData.response, data.response], + }; + }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useUpdateCollection = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: any) => { + const load = toast.loading(t("updating_collection")); + + const response = await fetch(`/api/v1/collections/${data.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + toast.dismiss(load); + + return response.json(); + }, + onSuccess: (data) => { + { + toast.success(t("updated")); + return queryClient.setQueryData(["collections"], (oldData: any) => { + return { + response: oldData.response.map((collection: any) => + collection.id === data.response.id ? data.response : collection + ), + }; + }); + } + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useDeleteCollection = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id: number) => { + const load = toast.loading(t("deleting_collection")); + + const response = await fetch(`/api/v1/collections/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + toast.dismiss(load); + + return response.json(); + }, + onSuccess: (data) => { + toast.success(t("deleted")); + return queryClient.setQueryData(["collections"], (oldData: any) => { + return { + response: oldData.response.filter( + (collection: any) => collection.id !== data.response.id + ), + }; + }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { + useCollections, + useCreateCollection, + useUpdateCollection, + useDeleteCollection, +}; diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts index b1e3b7c..992befe 100644 --- a/hooks/useCollectivePermissions.ts +++ b/hooks/useCollectivePermissions.ts @@ -1,10 +1,11 @@ import useAccountStore from "@/store/account"; -import useCollectionStore from "@/store/collections"; import { Member } from "@/types/global"; import { useEffect, useState } from "react"; +import { useCollections } from "./store/collections"; export default function useCollectivePermissions(collectionIds: number[]) { - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const { account } = useAccountStore(); diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index 4b0dd17..2da9d9e 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -1,4 +1,3 @@ -import useCollectionStore from "@/store/collections"; import { useEffect } from "react"; import { useSession } from "next-auth/react"; import useTagStore from "@/store/tags"; @@ -7,7 +6,7 @@ import useLocalSettingsStore from "@/store/localSettings"; export default function useInitialData() { const { status, data } = useSession(); - const { setCollections } = useCollectionStore(); + // const { setCollections } = useCollectionStore(); const { setTags } = useTagStore(); // const { setLinks } = useLinkStore(); const { account, setAccount } = useAccountStore(); @@ -24,7 +23,7 @@ export default function useInitialData() { // Get the rest of the data useEffect(() => { if (account.id && (!process.env.NEXT_PUBLIC_STRIPE || account.username)) { - setCollections(); + // setCollections(); setTags(); // setLinks(); } diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index 746897b..bf21c18 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -1,10 +1,11 @@ import useAccountStore from "@/store/account"; -import useCollectionStore from "@/store/collections"; import { Member } from "@/types/global"; import { useEffect, useState } from "react"; +import { useCollections } from "./store/collections"; export default function usePermissions(collectionId: number) { - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const { account } = useAccountStore(); diff --git a/package.json b/package.json index 1c428cf..f766af5 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@mozilla/readability": "^0.4.4", "@prisma/client": "^4.16.2", "@stripe/stripe-js": "^1.54.1", + "@tanstack/react-query": "^5.51.15", + "@tanstack/react-query-devtools": "^5.51.15", "@types/crypto-js": "^4.1.1", "@types/formidable": "^3.4.5", "@types/node": "^20.10.4", diff --git a/pages/_app.tsx b/pages/_app.tsx index 0cb79ff..45f3657 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -11,7 +11,10 @@ import { Session } from "next-auth"; import { isPWA } from "@/lib/client/utils"; // import useInitialData from "@/hooks/useInitialData"; import { appWithTranslation } from "next-i18next"; -import nextI18nextConfig from "../next-i18next.config"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); function App({ Component, @@ -29,72 +32,75 @@ function App({ }, []); return ( - - - Linkwarden - - - - - - - - - {/* */} - - {(t) => ( - - {({ icon, message }) => ( -
- {icon} - {message} - {t.type !== "loading" && ( - - )} -
- )} -
- )} -
- - {/*
*/} -
-
+ + + + Linkwarden + + + + + + + + + {/* */} + + {(t) => ( + + {({ icon, message }) => ( +
+ {icon} + {message} + {t.type !== "loading" && ( + + )} +
+ )} +
+ )} +
+ + {/*
*/} +
+
+ +
); } diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index bd24a95..ccbcae3 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -1,4 +1,3 @@ -import useCollectionStore from "@/store/collections"; import useLinkStore from "@/store/links"; import { CollectionIncludingMembersAndLinkCount, @@ -26,6 +25,7 @@ import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import LinkListOptions from "@/components/LinkListOptions"; +import { useCollections } from "@/hooks/store/collections"; export default function Index() { const { t } = useTranslation(); @@ -34,7 +34,8 @@ export default function Index() { const router = useRouter(); const { links } = useLinkStore(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 642e3dd..3314dfb 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -1,4 +1,3 @@ -import useCollectionStore from "@/store/collections"; import CollectionCard from "@/components/CollectionCard"; import { useState } from "react"; import MainLayout from "@/layouts/MainLayout"; @@ -10,10 +9,12 @@ import NewCollectionModal from "@/components/ModalContent/NewCollectionModal"; import PageHeader from "@/components/PageHeader"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; export default function Collections() { const { t } = useTranslation(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); const [sortedCollections, setSortedCollections] = useState(collections); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 3d1fdb6..40ee933 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -1,5 +1,4 @@ import useLinkStore from "@/store/links"; -import useCollectionStore from "@/store/collections"; import useTagStore from "@/store/tags"; import MainLayout from "@/layouts/MainLayout"; import { useEffect, useState } from "react"; @@ -19,10 +18,12 @@ import { dropdownTriggerer } from "@/lib/client/utils"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; +import { useCollections } from "@/hooks/store/collections"; export default function Dashboard() { const { t } = useTranslation(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const { links } = useLinkStore(); const { tags } = useTagStore(); diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 47e074f..faa4950 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -23,8 +23,8 @@ import ListView from "@/components/LinkViews/Layouts/ListView"; import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; -import useCollectionStore from "@/store/collections"; import LinkListOptions from "@/components/LinkListOptions"; +import { useCollections } from "@/hooks/store/collections"; export default function PublicCollections() { const { t } = useTranslation(); @@ -32,7 +32,8 @@ export default function PublicCollections() { const { settings } = useLocalSettingsStore(); - const { collections } = useCollectionStore(); + const { data: { response: collections } = { response: [] } } = + useCollections(); const router = useRouter(); diff --git a/store/collections.ts b/store/collections.ts deleted file mode 100644 index 466b652..0000000 --- a/store/collections.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { create } from "zustand"; -import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; -import useTagStore from "./tags"; - -type ResponseObject = { - ok: boolean; - data: object | string; -}; - -type CollectionStore = { - collections: CollectionIncludingMembersAndLinkCount[]; - setCollections: () => void; - addCollection: ( - body: CollectionIncludingMembersAndLinkCount - ) => Promise; - updateCollection: ( - collection: CollectionIncludingMembersAndLinkCount - ) => Promise; - removeCollection: (collectionId: number) => Promise; -}; - -const useCollectionStore = create()((set) => ({ - collections: [], - setCollections: async () => { - const response = await fetch("/api/v1/collections"); - - const data = await response.json(); - - if (response.ok) set({ collections: data.response }); - }, - addCollection: async (body) => { - const response = await fetch("/api/v1/collections", { - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); - - const data = await response.json(); - - if (response.ok) - set((state) => ({ - collections: [...state.collections, data.response], - })); - - return { ok: response.ok, data: data.response }; - }, - updateCollection: async (collection) => { - const response = await fetch(`/api/v1/collections/${collection.id}`, { - body: JSON.stringify(collection), - headers: { - "Content-Type": "application/json", - }, - method: "PUT", - }); - - const data = await response.json(); - - if (response.ok) - set((state) => ({ - collections: state.collections.map((e) => - e.id === data.response.id ? data.response : e - ), - })); - - return { ok: response.ok, data: data.response }; - }, - removeCollection: async (collectionId) => { - const response = await fetch(`/api/v1/collections/${collectionId}`, { - headers: { - "Content-Type": "application/json", - }, - method: "DELETE", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - collections: state.collections.filter( - (collection) => - collection.id !== collectionId && - collection.parentId !== collectionId - ), - })); - useTagStore.getState().setTags(); - } - - return { ok: response.ok, data: data.response }; - }, -})); - -export default useCollectionStore; diff --git a/yarn.lock b/yarn.lock index 943096e..174222e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1903,6 +1903,30 @@ dependencies: tslib "^2.4.0" +"@tanstack/query-core@5.51.15": + version "5.51.15" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.51.15.tgz#7aee6a2d5d3f64de3e54096607233b1132dc6afd" + integrity sha512-xyobHDJ0yhPE3+UkSQ2/4X1fLSg7ICJI5J1JyU9yf7F3deQfEwSImCDrB1WSRrauJkMtXW7YIEcC0oA6ZZWt5A== + +"@tanstack/query-devtools@5.51.15": + version "5.51.15" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.51.15.tgz#81c5c28231adc4b95fe4a5e1004020fdca5ea447" + integrity sha512-1oSCl+PsCa/aBCGVM2ZdcQLuQ0QYmKXJJB264twEMVM1M0n5CI40trtywORPF+wLGuZNIZzkKL7j/98mOLAIag== + +"@tanstack/react-query-devtools@^5.51.15": + version "5.51.15" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.51.15.tgz#5c4d21305fd25c35dc88bd280304f77a45554fc2" + integrity sha512-bvGvJoncjZ3irEofoFevptj5BPkDpQrp2+dZhtFqPUZXRT6MAKPmOqtSmZPfacLR5jQLpqw/7d3Zxr173z7WDA== + dependencies: + "@tanstack/query-devtools" "5.51.15" + +"@tanstack/react-query@^5.51.15": + version "5.51.15" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.51.15.tgz#059bb2966f828263adb355de81410d107e22b5bc" + integrity sha512-UgFg23SrdIYrmfTSxAUn9g+J64VQy11pb9/EefoY/u2+zWuNMeqEOnvpJhf52XQy0yztQoyM9p6x8PFyTNaxXg== + dependencies: + "@tanstack/query-core" "5.51.15" + "@tokenizer/token@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" From d1ed33b5327c56f101e42c07dfae73e86538c314 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 30 Jul 2024 14:59:18 -0400 Subject: [PATCH 06/33] bug fix --- store/links.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/store/links.ts b/store/links.ts index c2c3a8a..db9a1fc 100644 --- a/store/links.ts +++ b/store/links.ts @@ -4,7 +4,6 @@ import { LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; import useTagStore from "./tags"; -import useCollectionStore from "./collections"; type ResponseObject = { ok: boolean; @@ -81,7 +80,6 @@ const useLinkStore = create()((set) => ({ links: [data.response, ...state.links], })); useTagStore.getState().setTags(); - useCollectionStore.getState().setCollections(); } return { ok: response.ok, data: data.response }; @@ -157,7 +155,6 @@ const useLinkStore = create()((set) => ({ ], })); useTagStore.getState().setTags(); - useCollectionStore.getState().setCollections(); } return { ok: response.ok, data: data.response }; @@ -213,7 +210,6 @@ const useLinkStore = create()((set) => ({ ), })); useTagStore.getState().setTags(); - useCollectionStore.getState().setCollections(); } return { ok: response.ok, data: data.response }; @@ -248,7 +244,6 @@ const useLinkStore = create()((set) => ({ ), })); useTagStore.getState().setTags(); - useCollectionStore.getState().setCollections(); } return { ok: response.ok, data: data.response }; @@ -268,7 +263,6 @@ const useLinkStore = create()((set) => ({ links: state.links.filter((e) => e.id !== linkId), })); useTagStore.getState().setTags(); - useCollectionStore.getState().setCollections(); } return { ok: response.ok, data: data.response }; @@ -289,7 +283,6 @@ const useLinkStore = create()((set) => ({ links: state.links.filter((e) => !linkIds.includes(e.id as number)), })); useTagStore.getState().setTags(); - useCollectionStore.getState().setCollections(); } return { ok: response.ok, data: data.response }; From 5c5dd967c40315e208552262255abcd01d0cd64a Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 30 Jul 2024 23:19:29 -0400 Subject: [PATCH 07/33] refactor account store + much smoother collection listing updates --- components/CollectionCard.tsx | 22 +++--- components/CollectionListing.tsx | 68 ++++++++++--------- .../InputSelect/CollectionSelection.tsx | 3 +- components/LinkViews/LinkCard.tsx | 9 ++- .../LinkViews/LinkComponents/LinkActions.tsx | 6 +- components/LinkViews/LinkList.tsx | 9 ++- components/LinkViews/LinkMasonry.tsx | 9 ++- .../EditCollectionSharingModal.tsx | 8 +-- .../ModalContent/NewCollectionModal.tsx | 4 -- components/ModalContent/NewLinkModal.tsx | 3 +- .../ModalContent/PreservedFormatsModal.tsx | 22 +++--- components/ModalContent/UploadFileModal.tsx | 3 +- components/ProfileDropdown.tsx | 8 +-- components/ReadableView.tsx | 3 +- components/Sidebar.tsx | 3 +- hooks/store/collections.tsx | 62 ++++++++++------- hooks/store/users.tsx | 63 +++++++++++++++++ hooks/useCollectivePermissions.ts | 13 ++-- hooks/useInitialData.tsx | 12 ++-- hooks/usePermissions.tsx | 13 ++-- hooks/useReorderCollection.tsx | 0 layouts/AuthRedirect.tsx | 8 +-- pages/collections/[id].tsx | 25 ++++--- pages/collections/index.tsx | 3 +- pages/dashboard.tsx | 3 +- pages/public/collections/[id].tsx | 3 +- pages/settings/account.tsx | 36 +++++----- pages/settings/password.tsx | 35 +++++----- pages/settings/preference.tsx | 16 ++--- pages/subscribe.tsx | 6 +- store/account.ts | 41 ----------- 31 files changed, 260 insertions(+), 259 deletions(-) create mode 100644 hooks/store/users.tsx create mode 100644 hooks/useReorderCollection.tsx delete mode 100644 store/account.ts diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index da634cc..f57fcc0 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -5,12 +5,12 @@ import ProfilePhoto from "./ProfilePhoto"; import usePermissions from "@/hooks/usePermissions"; import useLocalSettingsStore from "@/store/localSettings"; import getPublicUserData from "@/lib/client/getPublicUserData"; -import useAccountStore from "@/store/account"; import EditCollectionModal from "./ModalContent/EditCollectionModal"; import EditCollectionSharingModal from "./ModalContent/EditCollectionSharingModal"; import DeleteCollectionModal from "./ModalContent/DeleteCollectionModal"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; +import { useUser } from "@/hooks/store/users"; type Props = { collection: CollectionIncludingMembersAndLinkCount; @@ -20,7 +20,7 @@ type Props = { export default function CollectionCard({ collection, className }: Props) { const { t } = useTranslation(); const { settings } = useLocalSettingsStore(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const formattedDate = new Date(collection.createdAt as string).toLocaleString( "en-US", @@ -45,18 +45,18 @@ export default function CollectionCard({ collection, className }: Props) { useEffect(() => { const fetchOwner = async () => { - if (collection && collection.ownerId !== account.id) { + if (collection && collection.ownerId !== user.id) { const owner = await getPublicUserData(collection.ownerId as number); setCollectionOwner(owner); - } else if (collection && collection.ownerId === account.id) { + } else if (collection && collection.ownerId === user.id) { setCollectionOwner({ - id: account.id as number, - name: account.name, - username: account.username as string, - image: account.image as string, - archiveAsScreenshot: account.archiveAsScreenshot as boolean, - archiveAsMonolith: account.archiveAsMonolith as boolean, - archiveAsPDF: account.archiveAsPDF as boolean, + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsMonolith as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, }); } }; diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index d3b433e..725c0d6 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -13,10 +13,10 @@ import { Collection } from "@prisma/client"; import Link from "next/link"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { useRouter } from "next/router"; -import useAccountStore from "@/store/account"; import toast from "react-hot-toast"; import { useTranslation } from "next-i18next"; import { useCollections, useUpdateCollection } from "@/hooks/store/collections"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; interface ExtendedTreeItem extends TreeItem { data: Collection; @@ -25,54 +25,56 @@ interface ExtendedTreeItem extends TreeItem { const CollectionListing = () => { const { t } = useTranslation(); const updateCollection = useUpdateCollection(); - const { data: { response: collections } = { response: [] } } = - useCollections(); - const { account, updateAccount } = useAccountStore(); + const { data: collections = [] } = useCollections(); + + const { data: user = [] } = useUser(); + const updateUser = useUpdateUser(); const router = useRouter(); const currentPath = router.asPath; + const [tree, setTree] = useState(); + const initialTree = useMemo(() => { - if (collections.length > 0) { + if ( + // !tree && + collections.length > 0 + ) { return buildTreeFromCollections( collections, router, - account.collectionOrder + user.collectionOrder ); - } - return undefined; - }, [collections, router]); - - const [tree, setTree] = useState(initialTree); + } else return undefined; + }, [collections, user, router]); useEffect(() => { + // if (!tree) setTree(initialTree); }, [initialTree]); useEffect(() => { - if (account.username) { + if (user.username) { if ( - (!account.collectionOrder || account.collectionOrder.length === 0) && + (!user.collectionOrder || user.collectionOrder.length === 0) && collections.length > 0 ) - updateAccount({ - ...account, + updateUser.mutate({ + ...user, collectionOrder: collections .filter( (e) => e.parentId === null || !collections.find((i) => i.id === e.parentId) ) // Filter out collections with non-null parentId - .map((e) => e.id as number), // Use "as number" to assert that e.id is a number + .map((e) => e.id as number), }); else { - const newCollectionOrder: number[] = [ - ...(account.collectionOrder || []), - ]; + const newCollectionOrder: number[] = [...(user.collectionOrder || [])]; // Start with collections that are in both account.collectionOrder and collections const existingCollectionIds = collections.map((c) => c.id as number); - const filteredCollectionOrder = account.collectionOrder.filter((id) => + const filteredCollectionOrder = user.collectionOrder.filter((id: any) => existingCollectionIds.includes(id) ); @@ -80,7 +82,7 @@ const CollectionListing = () => { collections.forEach((collection) => { if ( !filteredCollectionOrder.includes(collection.id as number) && - (!collection.parentId || collection.ownerId === account.id) + (!collection.parentId || collection.ownerId === user.id) ) { filteredCollectionOrder.push(collection.id as number); } @@ -89,10 +91,10 @@ const CollectionListing = () => { // check if the newCollectionOrder is the same as the old one if ( JSON.stringify(newCollectionOrder) !== - JSON.stringify(account.collectionOrder) + JSON.stringify(user.collectionOrder) ) { - updateAccount({ - ...account, + updateUser.mutateAsync({ + ...user, collectionOrder: newCollectionOrder, }); } @@ -140,9 +142,9 @@ const CollectionListing = () => { ); if ( - (movedCollection?.ownerId !== account.id && + (movedCollection?.ownerId !== user.id && destination.parentId !== source.parentId) || - (destinationCollection?.ownerId !== account.id && + (destinationCollection?.ownerId !== user.id && destination.parentId !== "root") ) { return toast.error(t("cant_change_collection_you_dont_own")); @@ -150,7 +152,7 @@ const CollectionListing = () => { setTree((currentTree) => moveItemOnTree(currentTree!, source, destination)); - const updatedCollectionOrder = [...account.collectionOrder]; + const updatedCollectionOrder = [...user.collectionOrder]; if (source.parentId !== destination.parentId) { await updateCollection.mutateAsync({ @@ -174,8 +176,8 @@ const CollectionListing = () => { updatedCollectionOrder.splice(destination.index, 0, movedCollectionId); - await updateAccount({ - ...account, + await updateUser.mutateAsync({ + ...user, collectionOrder: updatedCollectionOrder, }); } else if ( @@ -184,8 +186,8 @@ const CollectionListing = () => { ) { updatedCollectionOrder.splice(destination.index, 0, movedCollectionId); - await updateAccount({ - ...account, + updateUser.mutate({ + ...user, collectionOrder: updatedCollectionOrder, }); } else if ( @@ -195,8 +197,8 @@ const CollectionListing = () => { ) { updatedCollectionOrder.splice(source.index, 1); - await updateAccount({ - ...account, + await updateUser.mutateAsync({ + ...user, collectionOrder: updatedCollectionOrder, }); } diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 8a98b9e..81bef39 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -24,8 +24,7 @@ export default function CollectionSelection({ showDefaultValue = true, creatable = true, }: Props) { - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx index 20597b7..189dab4 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkCard.tsx @@ -15,12 +15,12 @@ import Link from "next/link"; import LinkIcon from "./LinkComponents/LinkIcon"; import useOnScreen from "@/hooks/useOnScreen"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -34,10 +34,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); const viewMode = localStorage.getItem("viewMode") || "card"; - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); @@ -133,7 +132,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
- !editMode && window.open(generateLinkHref(link, account), "_blank") + !editMode && window.open(generateLinkHref(link, user), "_blank") } >
diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index c68dd06..0d0b7eb 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -9,9 +9,9 @@ import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal"; import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal"; import useLinkStore from "@/store/links"; import { toast } from "react-hot-toast"; -import useAccountStore from "@/store/account"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -39,7 +39,7 @@ export default function LinkActions({ const [deleteLinkModal, setDeleteLinkModal] = useState(false); const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const { removeLink, updateLink } = useLinkStore(); @@ -50,7 +50,7 @@ export default function LinkActions({ const response = await updateLink({ ...link, - pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }], + pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }], }); toast.dismiss(load); diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index 936d857..71c70cf 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -11,12 +11,12 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon"; import { isPWA } from "@/lib/client/utils"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -33,10 +33,9 @@ export default function LinkCardCompact({ }: Props) { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const { links, setSelectedLinks, selectedLinks } = useLinkStore(); useEffect(() => { @@ -121,7 +120,7 @@ export default function LinkCardCompact({
- !editMode && window.open(generateLinkHref(link, account), "_blank") + !editMode && window.open(generateLinkHref(link, user), "_blank") } >
diff --git a/components/LinkViews/LinkMasonry.tsx b/components/LinkViews/LinkMasonry.tsx index c3c2451..8313eb3 100644 --- a/components/LinkViews/LinkMasonry.tsx +++ b/components/LinkViews/LinkMasonry.tsx @@ -15,12 +15,12 @@ import Link from "next/link"; import LinkIcon from "./LinkComponents/LinkIcon"; import useOnScreen from "@/hooks/useOnScreen"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -33,9 +33,8 @@ type Props = { export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); - const { account } = useAccountStore(); + const { data: collections = [] } = useCollections(); + const { data: user = [] } = useUser(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); @@ -131,7 +130,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
- !editMode && window.open(generateLinkHref(link, account), "_blank") + !editMode && window.open(generateLinkHref(link, user), "_blank") } >
diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index 10831d7..fd2207e 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -3,7 +3,6 @@ import TextInput from "@/components/TextInput"; import toast from "react-hot-toast"; import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import getPublicUserData from "@/lib/client/getPublicUserData"; -import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import ProfilePhoto from "../ProfilePhoto"; import addMemberToCollection from "@/lib/client/addMemberToCollection"; @@ -11,6 +10,7 @@ import Modal from "../Modal"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; import { useUpdateCollection } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; type Props = { onClose: Function; @@ -46,7 +46,7 @@ export default function EditCollectionSharingModal({ } }; - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const permissions = usePermissions(collection.id as number); const currentURL = new URL(document.URL); @@ -158,7 +158,7 @@ export default function EditCollectionSharingModal({ onKeyDown={(e) => e.key === "Enter" && addMemberToCollection( - account.username as string, + user.username as string, memberUsername || "", collection, setMemberState, @@ -170,7 +170,7 @@ export default function EditCollectionSharingModal({
addMemberToCollection( - account.username as string, + user.username as string, memberUsername || "", collection, setMemberState, diff --git a/components/ModalContent/NewCollectionModal.tsx b/components/ModalContent/NewCollectionModal.tsx index e1ab424..24284e2 100644 --- a/components/ModalContent/NewCollectionModal.tsx +++ b/components/ModalContent/NewCollectionModal.tsx @@ -4,8 +4,6 @@ import { HexColorPicker } from "react-colorful"; import { Collection } from "@prisma/client"; import Modal from "../Modal"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; -import useAccountStore from "@/store/account"; -import { useSession } from "next-auth/react"; import { useTranslation } from "next-i18next"; import { useCreateCollection } from "@/hooks/store/collections"; @@ -24,8 +22,6 @@ export default function NewCollectionModal({ onClose, parent }: Props) { } as Partial; const [collection, setCollection] = useState>(initial); - const { setAccount } = useAccountStore(); - const { data } = useSession(); useEffect(() => { setCollection(initial); diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 605d03c..5164789 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -43,8 +43,7 @@ export default function NewLinkModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 87e84a7..e83d749 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -16,10 +16,10 @@ import { screenshotAvailable, } from "@/lib/shared/getArchiveValidity"; import PreservedFormatRow from "@/components/PreserverdFormatRow"; -import useAccountStore from "@/store/account"; import getPublicUserData from "@/lib/client/getPublicUserData"; import { useTranslation } from "next-i18next"; import { BeatLoader } from "react-spinners"; +import { useUser } from "@/hooks/store/users"; type Props = { onClose: Function; @@ -30,7 +30,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { const { t } = useTranslation(); const session = useSession(); const { getLink } = useLinkStore(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [link, setLink] = useState(activeLink); const router = useRouter(); @@ -49,20 +49,20 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { useEffect(() => { const fetchOwner = async () => { - if (link.collection.ownerId !== account.id) { + if (link.collection.ownerId !== user.id) { const owner = await getPublicUserData( link.collection.ownerId as number ); setCollectionOwner(owner); - } else if (link.collection.ownerId === account.id) { + } else if (link.collection.ownerId === user.id) { setCollectionOwner({ - id: account.id as number, - name: account.name, - username: account.username as string, - image: account.image as string, - archiveAsScreenshot: account.archiveAsScreenshot as boolean, - archiveAsMonolith: account.archiveAsScreenshot as boolean, - archiveAsPDF: account.archiveAsPDF as boolean, + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsScreenshot as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, }); } }; diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index c5c3b5e..8cd2807 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -49,8 +49,7 @@ export default function UploadFileModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index 21ce54e..d610277 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -1,17 +1,17 @@ import useLocalSettingsStore from "@/store/localSettings"; import { dropdownTriggerer } from "@/lib/client/utils"; import ProfilePhoto from "./ProfilePhoto"; -import useAccountStore from "@/store/account"; import Link from "next/link"; import { signOut } from "next-auth/react"; import { useTranslation } from "next-i18next"; +import { useUser } from "@/hooks/store/users"; export default function ProfileDropdown() { const { t } = useTranslation(); const { settings, updateSettings } = useLocalSettingsStore(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); - const isAdmin = account.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1); + const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1); const handleToggle = () => { const newTheme = settings.theme === "dark" ? "light" : "dark"; @@ -27,7 +27,7 @@ export default function ProfileDropdown() { className="btn btn-circle btn-ghost" >
diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index e10461b..70e21fe 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -46,8 +46,7 @@ export default function ReadableView({ link }: Props) { const router = useRouter(); const { getLink } = useLinkStore(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const collection = useMemo(() => { return collections.find( diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 20cfd0b..d3cde02 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -22,8 +22,7 @@ export default function Sidebar({ className }: { className?: string }) { } ); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const { tags } = useTagStore(); const [active, setActive] = useState(""); diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx index 46109dc..fe79d1e 100644 --- a/hooks/store/collections.tsx +++ b/hooks/store/collections.tsx @@ -6,12 +6,10 @@ import toast from "react-hot-toast"; const useCollections = () => { return useQuery({ queryKey: ["collections"], - queryFn: async (): Promise<{ - response: CollectionIncludingMembersAndLinkCount[]; - }> => { + queryFn: async (): Promise => { const response = await fetch("/api/v1/collections"); const data = await response.json(); - return data; + return data.response; }, }); }; @@ -21,11 +19,11 @@ const useCreateCollection = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: any) => { + mutationFn: async (body: any) => { const load = toast.loading(t("creating")); const response = await fetch("/api/v1/collections", { - body: JSON.stringify(data), + body: JSON.stringify(body), headers: { "Content-Type": "application/json", }, @@ -34,14 +32,18 @@ const useCreateCollection = () => { toast.dismiss(load); - return response.json(); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; }, onSuccess: (data) => { toast.success(t("created")); return queryClient.setQueryData(["collections"], (oldData: any) => { - return { - response: [...oldData.response, data.response], - }; + console.log([...oldData, data]); + + return [...oldData, data]; }); }, onError: (error) => { @@ -55,33 +57,43 @@ const useUpdateCollection = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (data: any) => { + mutationFn: async (body: any) => { const load = toast.loading(t("updating_collection")); - const response = await fetch(`/api/v1/collections/${data.id}`, { + const response = await fetch(`/api/v1/collections/${body.id}`, { method: "PUT", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(data), + body: JSON.stringify(body), }); toast.dismiss(load); - return response.json(); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; }, onSuccess: (data) => { { toast.success(t("updated")); return queryClient.setQueryData(["collections"], (oldData: any) => { - return { - response: oldData.response.map((collection: any) => - collection.id === data.response.id ? data.response : collection - ), - }; + return oldData.map((collection: any) => + collection.id === data.id ? data : collection + ); }); } }, + // onMutate: async (data) => { + // await queryClient.cancelQueries({ queryKey: ["collections"] }); + // queryClient.setQueryData(["collections"], (oldData: any) => { + // return oldData.map((collection: any) => + // collection.id === data.id ? data : collection + // ) + // }); + // }, onError: (error) => { toast.error(error.message); }, @@ -105,16 +117,16 @@ const useDeleteCollection = () => { toast.dismiss(load); - return response.json(); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; }, onSuccess: (data) => { toast.success(t("deleted")); return queryClient.setQueryData(["collections"], (oldData: any) => { - return { - response: oldData.response.filter( - (collection: any) => collection.id !== data.response.id - ), - }; + return oldData.filter((collection: any) => collection.id !== data.id); }); }, onError: (error) => { diff --git a/hooks/store/users.tsx b/hooks/store/users.tsx new file mode 100644 index 0000000..881cf0a --- /dev/null +++ b/hooks/store/users.tsx @@ -0,0 +1,63 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; +import { useSession } from "next-auth/react"; + +const useUser = () => { + const { data } = useSession(); + + const userId = data?.user.id; + + return useQuery({ + queryKey: ["user"], + queryFn: async () => { + const response = await fetch(`/api/v1/users/${userId}`); + if (!response.ok) throw new Error("Failed to fetch user data."); + + const data = await response.json(); + + return data.response; + }, + enabled: !!userId, + }); +}; + +const useUpdateUser = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (user: any) => { + const load = toast.loading(t("applying_settings")); + + const response = await fetch(`/api/v1/users/${user.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(user), + }); + + const data = await response.json(); + + toast.dismiss(load); + + if (!response.ok) throw new Error(data.response); + + return data; + }, + onSuccess: (data) => { + toast.success(t("settings_applied")); + queryClient.setQueryData(["user"], data.response); + }, + onMutate: async (user) => { + await queryClient.cancelQueries({ queryKey: ["user"] }); + queryClient.setQueryData(["user"], (oldData: any) => { + return { ...oldData, ...user }; + }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { useUser, useUpdateUser }; diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts index 992befe..aebb842 100644 --- a/hooks/useCollectivePermissions.ts +++ b/hooks/useCollectivePermissions.ts @@ -1,13 +1,12 @@ -import useAccountStore from "@/store/account"; import { Member } from "@/types/global"; import { useEffect, useState } from "react"; import { useCollections } from "./store/collections"; +import { useUser } from "./store/users"; export default function useCollectivePermissions(collectionIds: number[]) { - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { @@ -16,7 +15,7 @@ export default function useCollectivePermissions(collectionIds: number[]) { if (collection) { let getPermission: Member | undefined = collection.members.find( - (e) => e.userId === account.id + (e) => e.userId === user.id ); if ( @@ -26,10 +25,10 @@ export default function useCollectivePermissions(collectionIds: number[]) { ) getPermission = undefined; - setPermissions(account.id === collection.ownerId || getPermission); + setPermissions(user.id === collection.ownerId || getPermission); } } - }, [account, collections, collectionIds]); + }, [user, collections, collectionIds]); return permissions; } diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index 2da9d9e..bb2686f 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -1,33 +1,29 @@ import { useEffect } from "react"; import { useSession } from "next-auth/react"; import useTagStore from "@/store/tags"; -import useAccountStore from "@/store/account"; import useLocalSettingsStore from "@/store/localSettings"; +import { useUser } from "./store/users"; export default function useInitialData() { const { status, data } = useSession(); // const { setCollections } = useCollectionStore(); const { setTags } = useTagStore(); // const { setLinks } = useLinkStore(); - const { account, setAccount } = useAccountStore(); + const { data: user = [] } = useUser(); const { setSettings } = useLocalSettingsStore(); useEffect(() => { setSettings(); - if (status === "authenticated") { - // Get account info - setAccount(data?.user.id as number); - } }, [status, data]); // Get the rest of the data useEffect(() => { - if (account.id && (!process.env.NEXT_PUBLIC_STRIPE || account.username)) { + if (user.id && (!process.env.NEXT_PUBLIC_STRIPE || user.username)) { // setCollections(); setTags(); // setLinks(); } - }, [account]); + }, [user]); return status; } diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index bf21c18..c6e806f 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -1,13 +1,12 @@ -import useAccountStore from "@/store/account"; import { Member } from "@/types/global"; import { useEffect, useState } from "react"; import { useCollections } from "./store/collections"; +import { useUser } from "./store/users"; export default function usePermissions(collectionId: number) { - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { @@ -15,7 +14,7 @@ export default function usePermissions(collectionId: number) { if (collection) { let getPermission: Member | undefined = collection.members.find( - (e) => e.userId === account.id + (e) => e.userId === user.id ); if ( @@ -25,9 +24,9 @@ export default function usePermissions(collectionId: number) { ) getPermission = undefined; - setPermissions(account.id === collection.ownerId || getPermission); + setPermissions(user.id === collection.ownerId || getPermission); } - }, [account, collections, collectionId]); + }, [user, collections, collectionId]); return permissions; } diff --git a/hooks/useReorderCollection.tsx b/hooks/useReorderCollection.tsx new file mode 100644 index 0000000..e69de29 diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 7f89222..69e372e 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -2,7 +2,7 @@ import { ReactNode, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { useSession } from "next-auth/react"; import useInitialData from "@/hooks/useInitialData"; -import useAccountStore from "@/store/account"; +import { useUser } from "@/hooks/store/users"; interface Props { children: ReactNode; @@ -14,7 +14,7 @@ export default function AuthRedirect({ children }: Props) { const router = useRouter(); const { status } = useSession(); const [shouldRenderChildren, setShouldRenderChildren] = useState(false); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); useInitialData(); @@ -23,7 +23,7 @@ export default function AuthRedirect({ children }: Props) { const isUnauthenticated = status === "unauthenticated"; const isPublicPage = router.pathname.startsWith("/public"); const hasInactiveSubscription = - account.id && !account.subscription?.active && stripeEnabled; + user.id && !user.subscription?.active && stripeEnabled; // There are better ways of doing this... but this one works for now const routes = [ @@ -63,7 +63,7 @@ export default function AuthRedirect({ children }: Props) { setShouldRenderChildren(true); } } - }, [status, account, router.pathname]); + }, [status, user, router.pathname]); function redirectTo(destination: string) { router.push(destination).then(() => setShouldRenderChildren(true)); diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index ccbcae3..e1bb1d9 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -12,7 +12,6 @@ import useLinks from "@/hooks/useLinks"; import usePermissions from "@/hooks/usePermissions"; import NoLinksFound from "@/components/NoLinksFound"; import useLocalSettingsStore from "@/store/localSettings"; -import useAccountStore from "@/store/account"; import getPublicUserData from "@/lib/client/getPublicUserData"; import EditCollectionModal from "@/components/ModalContent/EditCollectionModal"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; @@ -26,6 +25,7 @@ import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import LinkListOptions from "@/components/LinkListOptions"; import { useCollections } from "@/hooks/store/collections"; +import { useUser } from "@/hooks/store/users"; export default function Index() { const { t } = useTranslation(); @@ -34,8 +34,7 @@ export default function Index() { const router = useRouter(); const { links } = useLinkStore(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); @@ -52,7 +51,7 @@ export default function Index() { ); }, [router, collections]); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); const [collectionOwner, setCollectionOwner] = useState({ id: null as unknown as number, @@ -66,20 +65,20 @@ export default function Index() { useEffect(() => { const fetchOwner = async () => { - if (activeCollection && activeCollection.ownerId !== account.id) { + if (activeCollection && activeCollection.ownerId !== user.id) { const owner = await getPublicUserData( activeCollection.ownerId as number ); setCollectionOwner(owner); - } else if (activeCollection && activeCollection.ownerId === account.id) { + } else if (activeCollection && activeCollection.ownerId === user.id) { setCollectionOwner({ - id: account.id as number, - name: account.name, - username: account.username as string, - image: account.image as string, - archiveAsScreenshot: account.archiveAsScreenshot as boolean, - archiveAsMonolith: account.archiveAsScreenshot as boolean, - archiveAsPDF: account.archiveAsPDF as boolean, + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsScreenshot as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, }); } }; diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 3314dfb..f57ddfc 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -13,8 +13,7 @@ import { useCollections } from "@/hooks/store/collections"; export default function Collections() { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); const [sortedCollections, setSortedCollections] = useState(collections); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 40ee933..fe64928 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -22,8 +22,7 @@ import { useCollections } from "@/hooks/store/collections"; export default function Dashboard() { const { t } = useTranslation(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const { links } = useLinkStore(); const { tags } = useTagStore(); diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index faa4950..db06ec8 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -32,8 +32,7 @@ export default function PublicCollections() { const { settings } = useLocalSettingsStore(); - const { data: { response: collections } = { response: [] } } = - useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index cb46442..5783bf1 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import useAccountStore from "@/store/account"; import { AccountSettings } from "@/types/global"; import { toast } from "react-hot-toast"; import SettingsLayout from "@/layouts/SettingsLayout"; @@ -17,6 +16,7 @@ import Button from "@/components/ui/Button"; import { i18n } from "next-i18next.config"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; @@ -24,7 +24,8 @@ export default function Account() { const [emailChangeVerificationModal, setEmailChangeVerificationModal] = useState(false); const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); + const { data: account = [] } = useUser(); + const updateUser = useUpdateUser(); const [user, setUser] = useState( !objectIsEmpty(account) ? account @@ -78,25 +79,22 @@ export default function Account() { const submit = async (password?: string) => { setSubmitLoader(true); - const load = toast.loading(t("applying_settings")); - const response = await updateAccount({ - ...user, - // @ts-ignore - password: password ? password : undefined, - }); - - toast.dismiss(load); - - if (response.ok) { - const emailChanged = account.email !== user.email; - - toast.success(t("settings_applied")); - if (emailChanged) { - toast.success(t("email_change_request")); - setEmailChangeVerificationModal(false); + await updateUser.mutateAsync( + { + ...user, + password: password ? password : undefined, + }, + { + onSuccess: (data) => { + if (data.response.email !== user.email) { + toast.success(t("email_change_request")); + setEmailChangeVerificationModal(false); + } + }, } - } else toast.error(response.data as string); + ); + setSubmitLoader(false); }; diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx index 92f25ce..2c7c1b3 100644 --- a/pages/settings/password.tsx +++ b/pages/settings/password.tsx @@ -1,11 +1,11 @@ import SettingsLayout from "@/layouts/SettingsLayout"; import { useState } from "react"; -import useAccountStore from "@/store/account"; import SubmitButton from "@/components/SubmitButton"; import { toast } from "react-hot-toast"; import TextInput from "@/components/TextInput"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; export default function Password() { const { t } = useTranslation(); @@ -13,7 +13,8 @@ export default function Password() { const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); + const { data: account = [] } = useUser(); + const updateUser = useUpdateUser(); const submit = async () => { if (newPassword === "" || oldPassword === "") { @@ -23,23 +24,19 @@ export default function Password() { setSubmitLoader(true); - const load = toast.loading(t("applying_changes")); - - const response = await updateAccount({ - ...account, - newPassword, - oldPassword, - }); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("settings_applied")); - setNewPassword(""); - setOldPassword(""); - } else { - toast.error(response.data as string); - } + await updateUser.mutateAsync( + { + ...account, + newPassword, + oldPassword, + }, + { + onSuccess: () => { + setNewPassword(""); + setOldPassword(""); + }, + } + ); setSubmitLoader(false); }; diff --git a/pages/settings/preference.tsx b/pages/settings/preference.tsx index 55c0167..ae5840b 100644 --- a/pages/settings/preference.tsx +++ b/pages/settings/preference.tsx @@ -1,6 +1,5 @@ import SettingsLayout from "@/layouts/SettingsLayout"; import { useState, useEffect } from "react"; -import useAccountStore from "@/store/account"; import SubmitButton from "@/components/SubmitButton"; import { toast } from "react-hot-toast"; import Checkbox from "@/components/Checkbox"; @@ -8,12 +7,14 @@ import useLocalSettingsStore from "@/store/localSettings"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; // Import getServerSideProps for server-side data fetching import { LinksRouteTo } from "@prisma/client"; +import { useUpdateUser, useUser } from "@/hooks/store/users"; export default function Appearance() { const { t } = useTranslation(); const { updateSettings } = useLocalSettingsStore(); const [submitLoader, setSubmitLoader] = useState(false); - const { account, updateAccount } = useAccountStore(); + const { data: account = [] } = useUser(); + const updateUser = useUpdateUser(); const [user, setUser] = useState(account); const [preventDuplicateLinks, setPreventDuplicateLinks] = useState( @@ -73,17 +74,8 @@ export default function Appearance() { const submit = async () => { setSubmitLoader(true); - const load = toast.loading(t("applying_changes")); + await updateUser.mutateAsync({ ...user }); - const response = await updateAccount({ ...user }); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("settings_applied")); - } else { - toast.error(response.data as string); - } setSubmitLoader(false); }; diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index 591863e..c795da9 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -7,7 +7,7 @@ import { Plan } from "@/types/global"; import Button from "@/components/ui/Button"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { Trans, useTranslation } from "next-i18next"; -import useAccountStore from "@/store/account"; +import { useUser } from "@/hooks/store/users"; const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true"; @@ -20,11 +20,11 @@ export default function Subscribe() { const router = useRouter(); - const { account } = useAccountStore(); + const { data: user = [] } = useUser(); useEffect(() => { const hasInactiveSubscription = - account.id && !account.subscription?.active && stripeEnabled; + user.id && !user.subscription?.active && stripeEnabled; if (session.status === "authenticated" && !hasInactiveSubscription) { router.push("/dashboard"); diff --git a/store/account.ts b/store/account.ts deleted file mode 100644 index c1da399..0000000 --- a/store/account.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { create } from "zustand"; -import { AccountSettings } from "@/types/global"; - -type ResponseObject = { - ok: boolean; - data: Omit | object | string; -}; - -type AccountStore = { - account: AccountSettings; - setAccount: (id: number) => void; - updateAccount: (user: AccountSettings) => Promise; -}; - -const useAccountStore = create()((set) => ({ - account: {} as AccountSettings, - setAccount: async (id) => { - const response = await fetch(`/api/v1/users/${id}`); - - const data = await response.json(); - - if (response.ok) set({ account: { ...data.response } }); - }, - updateAccount: async (user) => { - const response = await fetch(`/api/v1/users/${user.id}`, { - method: "PUT", - body: JSON.stringify(user), - headers: { - "Content-Type": "application/json", - }, - }); - - const data = await response.json(); - - if (response.ok) set({ account: { ...data.response } }); - - return { ok: response.ok, data: data.response }; - }, -})); - -export default useAccountStore; From 099bc9e054da4ad143661987fde95958233e41a2 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 30 Jul 2024 23:23:58 -0400 Subject: [PATCH 08/33] remove old code --- store/modals.ts | 64 ------------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 store/modals.ts diff --git a/store/modals.ts b/store/modals.ts deleted file mode 100644 index f709d70..0000000 --- a/store/modals.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - CollectionIncludingMembersAndLinkCount, - LinkIncludingShortenedCollectionAndTags, -} from "@/types/global"; -import { create } from "zustand"; - -type Modal = - | { - modal: "LINK"; - state: boolean; - method: "CREATE"; - active?: LinkIncludingShortenedCollectionAndTags; - } - | { - modal: "LINK"; - state: boolean; - method: "UPDATE"; - active: LinkIncludingShortenedCollectionAndTags; - } - | { - modal: "LINK"; - state: boolean; - method: "FORMATS"; - active: LinkIncludingShortenedCollectionAndTags; - } - | { - modal: "COLLECTION"; - state: boolean; - method: "UPDATE"; - isOwner: boolean; - active: CollectionIncludingMembersAndLinkCount; - defaultIndex?: number; - } - | { - modal: "COLLECTION"; - state: boolean; - method: "CREATE"; - isOwner?: boolean; - active?: CollectionIncludingMembersAndLinkCount; - defaultIndex?: number; - } - | { - modal: "COLLECTION"; - state: boolean; - method: "VIEW_TEAM"; - isOwner?: boolean; - active?: CollectionIncludingMembersAndLinkCount; - defaultIndex?: number; - } - | null; - -type ModalsStore = { - modal: Modal; - setModal: (modal: Modal) => void; -}; - -const useModalStore = create((set) => ({ - modal: null, - setModal: (modal: Modal) => { - set({ modal }); - }, -})); - -export default useModalStore; From be5400f7cb024a3cd5503475d9000dc179931bfc Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Wed, 31 Jul 2024 14:15:50 -0400 Subject: [PATCH 09/33] rename users hook to user --- components/CollectionCard.tsx | 2 +- components/CollectionListing.tsx | 2 +- components/LinkViews/LinkCard.tsx | 2 +- components/LinkViews/LinkComponents/LinkActions.tsx | 2 +- components/LinkViews/LinkList.tsx | 2 +- components/LinkViews/LinkMasonry.tsx | 2 +- components/ModalContent/EditCollectionSharingModal.tsx | 2 +- components/ModalContent/PreservedFormatsModal.tsx | 2 +- components/ProfileDropdown.tsx | 2 +- hooks/store/{users.tsx => user.tsx} | 0 hooks/useCollectivePermissions.ts | 2 +- hooks/useInitialData.tsx | 2 +- hooks/usePermissions.tsx | 2 +- layouts/AuthRedirect.tsx | 2 +- pages/collections/[id].tsx | 2 +- pages/settings/account.tsx | 2 +- pages/settings/password.tsx | 2 +- pages/settings/preference.tsx | 2 +- pages/subscribe.tsx | 2 +- 19 files changed, 18 insertions(+), 18 deletions(-) rename hooks/store/{users.tsx => user.tsx} (100%) diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index f57fcc0..bb3a3dd 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -10,7 +10,7 @@ import EditCollectionSharingModal from "./ModalContent/EditCollectionSharingModa import DeleteCollectionModal from "./ModalContent/DeleteCollectionModal"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; type Props = { collection: CollectionIncludingMembersAndLinkCount; diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index 725c0d6..75b12cc 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -16,7 +16,7 @@ import { useRouter } from "next/router"; import toast from "react-hot-toast"; import { useTranslation } from "next-i18next"; import { useCollections, useUpdateCollection } from "@/hooks/store/collections"; -import { useUpdateUser, useUser } from "@/hooks/store/users"; +import { useUpdateUser, useUser } from "@/hooks/store/user"; interface ExtendedTreeItem extends TreeItem { data: Collection; diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx index 189dab4..e28ee9c 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkCard.tsx @@ -20,7 +20,7 @@ import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; type Props = { link: LinkIncludingShortenedCollectionAndTags; diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index 0d0b7eb..0a3775e 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -11,7 +11,7 @@ import useLinkStore from "@/store/links"; import { toast } from "react-hot-toast"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; type Props = { link: LinkIncludingShortenedCollectionAndTags; diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index 71c70cf..9755ad9 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -16,7 +16,7 @@ import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; type Props = { link: LinkIncludingShortenedCollectionAndTags; diff --git a/components/LinkViews/LinkMasonry.tsx b/components/LinkViews/LinkMasonry.tsx index 8313eb3..ca05f1a 100644 --- a/components/LinkViews/LinkMasonry.tsx +++ b/components/LinkViews/LinkMasonry.tsx @@ -20,7 +20,7 @@ import toast from "react-hot-toast"; import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; type Props = { link: LinkIncludingShortenedCollectionAndTags; diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index fd2207e..158f817 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -10,7 +10,7 @@ import Modal from "../Modal"; import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; import { useUpdateCollection } from "@/hooks/store/collections"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; type Props = { onClose: Function; diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index e83d749..30bd7b6 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -19,7 +19,7 @@ import PreservedFormatRow from "@/components/PreserverdFormatRow"; import getPublicUserData from "@/lib/client/getPublicUserData"; import { useTranslation } from "next-i18next"; import { BeatLoader } from "react-spinners"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; type Props = { onClose: Function; diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index d610277..2f262c2 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -4,7 +4,7 @@ import ProfilePhoto from "./ProfilePhoto"; import Link from "next/link"; import { signOut } from "next-auth/react"; import { useTranslation } from "next-i18next"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; export default function ProfileDropdown() { const { t } = useTranslation(); diff --git a/hooks/store/users.tsx b/hooks/store/user.tsx similarity index 100% rename from hooks/store/users.tsx rename to hooks/store/user.tsx diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts index aebb842..e093b64 100644 --- a/hooks/useCollectivePermissions.ts +++ b/hooks/useCollectivePermissions.ts @@ -1,7 +1,7 @@ import { Member } from "@/types/global"; import { useEffect, useState } from "react"; import { useCollections } from "./store/collections"; -import { useUser } from "./store/users"; +import { useUser } from "./store/user"; export default function useCollectivePermissions(collectionIds: number[]) { const { data: collections = [] } = useCollections(); diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index bb2686f..cf91623 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useSession } from "next-auth/react"; import useTagStore from "@/store/tags"; import useLocalSettingsStore from "@/store/localSettings"; -import { useUser } from "./store/users"; +import { useUser } from "./store/user"; export default function useInitialData() { const { status, data } = useSession(); diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index c6e806f..7b6725a 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -1,7 +1,7 @@ import { Member } from "@/types/global"; import { useEffect, useState } from "react"; import { useCollections } from "./store/collections"; -import { useUser } from "./store/users"; +import { useUser } from "./store/user"; export default function usePermissions(collectionId: number) { const { data: collections = [] } = useCollections(); diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 69e372e..821065a 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -2,7 +2,7 @@ import { ReactNode, useEffect, useState } from "react"; import { useRouter } from "next/router"; import { useSession } from "next-auth/react"; import useInitialData from "@/hooks/useInitialData"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; interface Props { children: ReactNode; diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index e1bb1d9..0c7ad8e 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -25,7 +25,7 @@ import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import LinkListOptions from "@/components/LinkListOptions"; import { useCollections } from "@/hooks/store/collections"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; export default function Index() { const { t } = useTranslation(); diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 5783bf1..5c4b109 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -16,7 +16,7 @@ import Button from "@/components/ui/Button"; import { i18n } from "next-i18next.config"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; -import { useUpdateUser, useUser } from "@/hooks/store/users"; +import { useUpdateUser, useUser } from "@/hooks/store/user"; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx index 2c7c1b3..d63daf4 100644 --- a/pages/settings/password.tsx +++ b/pages/settings/password.tsx @@ -5,7 +5,7 @@ import { toast } from "react-hot-toast"; import TextInput from "@/components/TextInput"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; -import { useUpdateUser, useUser } from "@/hooks/store/users"; +import { useUpdateUser, useUser } from "@/hooks/store/user"; export default function Password() { const { t } = useTranslation(); diff --git a/pages/settings/preference.tsx b/pages/settings/preference.tsx index ae5840b..18baee7 100644 --- a/pages/settings/preference.tsx +++ b/pages/settings/preference.tsx @@ -7,7 +7,7 @@ import useLocalSettingsStore from "@/store/localSettings"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; // Import getServerSideProps for server-side data fetching import { LinksRouteTo } from "@prisma/client"; -import { useUpdateUser, useUser } from "@/hooks/store/users"; +import { useUpdateUser, useUser } from "@/hooks/store/user"; export default function Appearance() { const { t } = useTranslation(); diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index c795da9..fb2ad3a 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -7,7 +7,7 @@ import { Plan } from "@/types/global"; import Button from "@/components/ui/Button"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { Trans, useTranslation } from "next-i18next"; -import { useUser } from "@/hooks/store/users"; +import { useUser } from "@/hooks/store/user"; const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true"; From 299a2331ff02190e497bc6b409c383433c6baeed Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 1 Aug 2024 15:54:00 -0400 Subject: [PATCH 10/33] Update README.md --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c3f0bc2..e8651d4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@

Linkwarden

+

Bookmark Preservation for Individuals and Teams

Discord -Twitter - -GitHub commits since latest release +Twitter
-[Website](https://linkwarden.app) | [Getting Started](https://docs.linkwarden.app) | [Features](https://github.com/linkwarden/linkwarden#features) | [Roadmap](https://github.com/orgs/linkwarden/projects/1) | [Support ❤](https://github.com/linkwarden/linkwarden#support-) +[« LAUNCH DEMO »](https://demo.linkwarden.app) + +[Cloud](https://cloud.linkwarden.app) · [Website](https://linkwarden.app) · [Features](https://github.com/linkwarden/linkwarden#features)
@@ -24,7 +25,7 @@ The objective is to organize useful webpages and articles you find across the we Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly. > [!TIP] -> Our official [Cloud](https://linkwarden.app/#pricing) offering provides the simplest way to begin using Linkwarden and it's the preferred choice for many due to its time-saving benefits.
Your subscription supports our hosting infrastructure and ongoing development.
Alternatively, if you prefer [self-hosting](https://docs.linkwarden.app/self-hosting/installation) Linkwarden, no problem! You'll still have access to all the premium features. +> Our official [Cloud](https://linkwarden.app/#pricing) offering provides the simplest way to begin using Linkwarden and it's the preferred choice for many due to its time-saving benefits.
Your subscription supports our hosting infrastructure and ongoing development.
Alternatively, if you prefer self-hosting Linkwarden, you can do so by following our [Installation documentation](https://docs.linkwarden.app/self-hosting/installation). @@ -71,10 +72,14 @@ We've forked the old version from the current repository into [this repo](https: - ⬇️ Import and export your bookmarks. - 🔐 SSO integration. (Enterprise and Self-hosted users only) - 📦 Installable Progressive Web App (PWA). +- 🍏 iOS and MacOS Apps, maintained by [JGeek00](https://github.com/JGeek00). - 🍎 iOS Shortcut to save links to Linkwarden. - 🔑 API keys. - ✅ Bulk actions. -- ✨ And so many more features! +- 👥 User administration. +- 🌐 Support for Other Languages (i18n). +- 📁 Image and PDF Uploads. +- ✨ And many more features. (Literally!) ## Like what we're doing? Give us a Star ⭐ @@ -98,7 +103,7 @@ We _usually_ go after the [popular suggestions](https://github.com/linkwarden/li Make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/projects/1). -## Docs +## Documentation For information on how to get started or to set up your own instance, please visit the [documentation](https://docs.linkwarden.app). @@ -110,7 +115,7 @@ If you want to contribute, Thanks! Start by checking our [public roadmap](https: If you found a security vulnerability, please do **not** create a public issue, instead send an email to [security@linkwarden.app](mailto:security@linkwarden.app) stating the vulnerability. Thanks! -## Support ❤ +## Support <3 Other than using our official [Cloud](https://linkwarden.app/#pricing) offering, any [donations](https://opencollective.com/linkwarden) are highly appreciated as well! From e889509697ce85b05aade7d9534f49234752766d Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 1 Aug 2024 16:54:19 -0400 Subject: [PATCH 11/33] refactor (admin/)users store --- components/ModalContent/DeleteUserModal.tsx | 28 +++---- components/ModalContent/NewUserModal.tsx | 25 +++--- hooks/store/admin/users.tsx | 89 +++++++++++++++++++++ pages/admin.tsx | 12 +-- store/admin/users.ts | 66 --------------- 5 files changed, 116 insertions(+), 104 deletions(-) create mode 100644 hooks/store/admin/users.tsx delete mode 100644 store/admin/users.ts diff --git a/components/ModalContent/DeleteUserModal.tsx b/components/ModalContent/DeleteUserModal.tsx index 6dc7ee2..7323373 100644 --- a/components/ModalContent/DeleteUserModal.tsx +++ b/components/ModalContent/DeleteUserModal.tsx @@ -1,8 +1,8 @@ -import toast from "react-hot-toast"; import Modal from "../Modal"; -import useUserStore from "@/store/admin/users"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; +import { useDeleteUser } from "@/hooks/store/admin/users"; +import { useState } from "react"; type Props = { onClose: Function; @@ -11,22 +11,22 @@ type Props = { export default function DeleteUserModal({ onClose, userId }: Props) { const { t } = useTranslation(); - const { removeUser } = useUserStore(); - const deleteUser = async () => { - const load = toast.loading(t("deleting_user")); + const [submitLoader, setSubmitLoader] = useState(false); + const deleteUser = useDeleteUser(); - const response = await removeUser(userId); + const submit = async () => { + if (!submitLoader) { + setSubmitLoader(true); - toast.dismiss(load); + await deleteUser.mutateAsync(userId, { + onSuccess: () => { + onClose(); + }, + }); - if (response.ok) { - toast.success(t("user_deleted")); - } else { - toast.error(response.data as string); + setSubmitLoader(false); } - - onClose(); }; return ( @@ -45,7 +45,7 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
- diff --git a/components/ModalContent/NewUserModal.tsx b/components/ModalContent/NewUserModal.tsx index d83ef40..78c36f7 100644 --- a/components/ModalContent/NewUserModal.tsx +++ b/components/ModalContent/NewUserModal.tsx @@ -1,9 +1,9 @@ import toast from "react-hot-toast"; import Modal from "../Modal"; -import useUserStore from "@/store/admin/users"; import TextInput from "../TextInput"; import { FormEvent, useState } from "react"; import { useTranslation, Trans } from "next-i18next"; +import { useAddUser } from "@/hooks/store/admin/users"; type Props = { onClose: Function; @@ -20,7 +20,9 @@ const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true"; export default function NewUserModal({ onClose }: Props) { const { t } = useTranslation(); - const { addUser } = useUserStore(); + + const addUser = useAddUser(); + const [form, setForm] = useState({ name: "", username: "", @@ -44,24 +46,15 @@ export default function NewUserModal({ onClose }: Props) { }; if (checkFields()) { - if (form.password.length < 8) - return toast.error(t("password_length_error")); - setSubmitLoader(true); - const load = toast.loading(t("creating_account")); + await addUser.mutateAsync(form, { + onSuccess: () => { + onClose(); + }, + }); - const response = await addUser(form); - - toast.dismiss(load); setSubmitLoader(false); - - if (response.ok) { - toast.success(t("user_created")); - onClose(); - } else { - toast.error(response.data as string); - } } else { toast.error(t("fill_all_fields_error")); } diff --git a/hooks/store/admin/users.tsx b/hooks/store/admin/users.tsx new file mode 100644 index 0000000..ec1e0fd --- /dev/null +++ b/hooks/store/admin/users.tsx @@ -0,0 +1,89 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; + +const useUsers = () => { + return useQuery({ + queryKey: ["users"], + queryFn: async () => { + const response = await fetch("/api/v1/users"); + if (!response.ok) { + if (response.status === 401) { + window.location.href = "/dashboard"; + } + throw new Error("Failed to fetch users."); + } + + const data = await response.json(); + return data.response; + }, + }); +}; + +const useAddUser = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (body: any) => { + if (body.password.length < 8) throw new Error(t("password_length_error")); + + const load = toast.loading(t("creating_account")); + + const response = await fetch("/api/v1/users", { + method: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data) => { + queryClient.setQueryData(["users"], (oldData: any) => [...oldData, data]); + toast.success(t("user_created")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useDeleteUser = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (userId: number) => { + const load = toast.loading(t("deleting_user")); + + const response = await fetch(`/api/v1/users/${userId}`, { + method: "DELETE", + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data, variables) => { + queryClient.setQueryData(["users"], (oldData: any) => + oldData.filter((user: any) => user.id !== variables) + ); + toast.success(t("user_deleted")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { useUsers, useAddUser, useDeleteUser }; diff --git a/pages/admin.tsx b/pages/admin.tsx index 7211e08..df62ffe 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -1,11 +1,11 @@ import NewUserModal from "@/components/ModalContent/NewUserModal"; -import useUserStore from "@/store/admin/users"; import { User as U } from "@prisma/client"; import Link from "next/link"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import UserListing from "@/components/UserListing"; +import { useUsers } from "@/hooks/store/admin/users"; interface User extends U { subscriptions: { @@ -21,7 +21,7 @@ type UserModal = { export default function Admin() { const { t } = useTranslation(); - const { users, setUsers } = useUserStore(); + const { data: users = [] } = useUsers(); const [searchQuery, setSearchQuery] = useState(""); const [filteredUsers, setFilteredUsers] = useState(); @@ -33,10 +33,6 @@ export default function Admin() { const [newUserModal, setNewUserModal] = useState(false); - useEffect(() => { - setUsers(); - }, []); - return (
@@ -71,7 +67,7 @@ export default function Admin() { if (users) { setFilteredUsers( - users.filter((user) => + users.filter((user: any) => JSON.stringify(user) .toLowerCase() .includes(e.target.value.toLowerCase()) diff --git a/store/admin/users.ts b/store/admin/users.ts deleted file mode 100644 index 6925611..0000000 --- a/store/admin/users.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { User as U } from "@prisma/client"; -import { create } from "zustand"; - -interface User extends U { - subscriptions: { - active: boolean; - }; -} - -type ResponseObject = { - ok: boolean; - data: object | string; -}; - -type UserStore = { - users: User[]; - setUsers: () => void; - addUser: (body: Partial) => Promise; - removeUser: (userId: number) => Promise; -}; - -const useUserStore = create((set) => ({ - users: [], - setUsers: async () => { - const response = await fetch("/api/v1/users"); - - const data = await response.json(); - - if (response.ok) set({ users: data.response }); - else if (response.status === 401) window.location.href = "/dashboard"; - }, - addUser: async (body) => { - const response = await fetch("/api/v1/users", { - method: "POST", - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - }, - }); - - const data = await response.json(); - - if (response.ok) - set((state) => ({ - users: [...state.users, data.response], - })); - - return { ok: response.ok, data: data.response }; - }, - removeUser: async (userId) => { - const response = await fetch(`/api/v1/users/${userId}`, { - method: "DELETE", - }); - - const data = await response.json(); - - if (response.ok) - set((state) => ({ - users: state.users.filter((user) => user.id !== userId), - })); - - return { ok: response.ok, data: data.response }; - }, -})); - -export default useUserStore; From da8dc83b8f1c5637cd330d91e559fbab94f6ecfe Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 1 Aug 2024 17:23:51 -0400 Subject: [PATCH 12/33] refactor tags store --- components/InputSelect/TagSelection.tsx | 6 +- components/Sidebar.tsx | 8 +-- hooks/store/tags.tsx | 87 +++++++++++++++++++++++++ hooks/useInitialData.tsx | 5 -- pages/dashboard.tsx | 4 +- pages/tags/[id].tsx | 36 ++++------ store/links.ts | 7 -- store/tags.ts | 62 ------------------ 8 files changed, 108 insertions(+), 107 deletions(-) create mode 100644 hooks/store/tags.tsx delete mode 100644 store/tags.ts diff --git a/components/InputSelect/TagSelection.tsx b/components/InputSelect/TagSelection.tsx index d901b40..efd246b 100644 --- a/components/InputSelect/TagSelection.tsx +++ b/components/InputSelect/TagSelection.tsx @@ -1,8 +1,8 @@ -import useTagStore from "@/store/tags"; import { useEffect, useState } from "react"; import CreatableSelect from "react-select/creatable"; import { styles } from "./styles"; import { Options } from "./types"; +import { useTags } from "@/hooks/store/tags"; type Props = { onChange: any; @@ -13,12 +13,12 @@ type Props = { }; export default function TagSelection({ onChange, defaultValue }: Props) { - const { tags } = useTagStore(); + const { data: tags = [] } = useTags(); const [options, setOptions] = useState([]); useEffect(() => { - const formatedCollections = tags.map((e) => { + const formatedCollections = tags.map((e: any) => { return { value: e.id, label: e.name }; }); diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index d3cde02..4348571 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -1,4 +1,3 @@ -import useTagStore from "@/store/tags"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -7,6 +6,7 @@ import SidebarHighlightLink from "@/components/SidebarHighlightLink"; import CollectionListing from "@/components/CollectionListing"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useTags } from "@/hooks/store/tags"; export default function Sidebar({ className }: { className?: string }) { const { t } = useTranslation(); @@ -24,7 +24,7 @@ export default function Sidebar({ className }: { className?: string }) { const { data: collections = [] } = useCollections(); - const { tags } = useTagStore(); + const { data: tags = [] } = useTags(); const [active, setActive] = useState(""); const router = useRouter(); @@ -130,8 +130,8 @@ export default function Sidebar({ className }: { className?: string }) { {tags[0] ? ( tags - .sort((a, b) => a.name.localeCompare(b.name)) - .map((e, i) => { + .sort((a: any, b: any) => a.name.localeCompare(b.name)) + .map((e: any, i: any) => { return (
{ + return useQuery({ + queryKey: ["tags"], + queryFn: async () => { + const response = await fetch("/api/v1/tags"); + if (!response.ok) throw new Error("Failed to fetch tags."); + + const data = await response.json(); + return data.response; + }, + }); +}; + +const useUpdateTag = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (tag: TagIncludingLinkCount) => { + const load = toast.loading(t("applying_changes")); + + const response = await fetch(`/api/v1/tags/${tag.id}`, { + body: JSON.stringify(tag), + headers: { + "Content-Type": "application/json", + }, + method: "PUT", + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data) => { + queryClient.setQueryData(["tags"], (oldData: any) => + oldData.map((tag: TagIncludingLinkCount) => + tag.id === data.id ? data : tag + ) + ); + toast.success(t("tag_renamed")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useRemoveTag = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (tagId: number) => { + const load = toast.loading(t("applying_changes")); + + const response = await fetch(`/api/v1/tags/${tagId}`, { + method: "DELETE", + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data, variables) => { + queryClient.setQueryData(["tags"], (oldData: any) => + oldData.filter((tag: TagIncludingLinkCount) => tag.id !== variables) + ); + toast.success(t("tag_deleted")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { useTags, useUpdateTag, useRemoveTag }; diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index cf91623..fab5241 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -1,13 +1,10 @@ import { useEffect } from "react"; import { useSession } from "next-auth/react"; -import useTagStore from "@/store/tags"; import useLocalSettingsStore from "@/store/localSettings"; import { useUser } from "./store/user"; export default function useInitialData() { const { status, data } = useSession(); - // const { setCollections } = useCollectionStore(); - const { setTags } = useTagStore(); // const { setLinks } = useLinkStore(); const { data: user = [] } = useUser(); const { setSettings } = useLocalSettingsStore(); @@ -19,8 +16,6 @@ export default function useInitialData() { // Get the rest of the data useEffect(() => { if (user.id && (!process.env.NEXT_PUBLIC_STRIPE || user.username)) { - // setCollections(); - setTags(); // setLinks(); } }, [user]); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index fe64928..adfa163 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -1,5 +1,4 @@ import useLinkStore from "@/store/links"; -import useTagStore from "@/store/tags"; import MainLayout from "@/layouts/MainLayout"; import { useEffect, useState } from "react"; import useLinks from "@/hooks/useLinks"; @@ -19,12 +18,13 @@ import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useTags } from "@/hooks/store/tags"; export default function Dashboard() { const { t } = useTranslation(); const { data: collections = [] } = useCollections(); const { links } = useLinkStore(); - const { tags } = useTagStore(); + const { data: tags = [] } = useTags(); const [numberOfLinks, setNumberOfLinks] = useState(0); diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 86df965..2467da6 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -2,7 +2,6 @@ import useLinkStore from "@/store/links"; import { useRouter } from "next/router"; import { FormEvent, useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; -import useTagStore from "@/store/tags"; import { Sort, TagIncludingLinkCount, ViewMode } from "@/types/global"; import useLinks from "@/hooks/useLinks"; import { toast } from "react-hot-toast"; @@ -15,13 +14,16 @@ import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; +import { useRemoveTag, useTags, useUpdateTag } from "@/hooks/store/tags"; export default function Index() { const { t } = useTranslation(); const router = useRouter(); const { links } = useLinkStore(); - const { tags, updateTag, removeTag } = useTagStore(); + const { data: tags = [] } = useTags(); + const updateTag = useUpdateTag(); + const removeTag = useRemoveTag(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); @@ -41,7 +43,7 @@ export default function Index() { useLinks({ tagId: Number(router.query.id), sort: sortBy }); useEffect(() => { - const tag = tags.find((e) => e.id === Number(router.query.id)); + const tag = tags.find((e: any) => e.id === Number(router.query.id)); if (tags.length > 0 && !tag?.id) { router.push("/dashboard"); @@ -72,21 +74,12 @@ export default function Index() { setSubmitLoader(true); - const load = toast.loading(t("applying_changes")); - - let response; - if (activeTag && newTagName) - response = await updateTag({ + await updateTag.mutateAsync({ ...activeTag, name: newTagName, }); - toast.dismiss(load); - - if (response?.ok) { - toast.success(t("tag_renamed")); - } else toast.error(response?.data as string); setSubmitLoader(false); setRenameTag(false); }; @@ -94,18 +87,13 @@ export default function Index() { const remove = async () => { setSubmitLoader(true); - const load = toast.loading(t("applying_changes")); + if (activeTag?.id) + await removeTag.mutateAsync(activeTag?.id, { + onSuccess: () => { + router.push("/links"); + }, + }); - let response; - - if (activeTag?.id) response = await removeTag(activeTag?.id); - - toast.dismiss(load); - - if (response?.ok) { - toast.success(t("tag_deleted")); - router.push("/links"); - } else toast.error(response?.data as string); setSubmitLoader(false); setRenameTag(false); }; diff --git a/store/links.ts b/store/links.ts index db9a1fc..9339b6e 100644 --- a/store/links.ts +++ b/store/links.ts @@ -3,7 +3,6 @@ import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; -import useTagStore from "./tags"; type ResponseObject = { ok: boolean; @@ -79,7 +78,6 @@ const useLinkStore = create()((set) => ({ set((state) => ({ links: [data.response, ...state.links], })); - useTagStore.getState().setTags(); } return { ok: response.ok, data: data.response }; @@ -154,7 +152,6 @@ const useLinkStore = create()((set) => ({ ...state.links, ], })); - useTagStore.getState().setTags(); } return { ok: response.ok, data: data.response }; @@ -209,7 +206,6 @@ const useLinkStore = create()((set) => ({ e.id === data.response.id ? data.response : e ), })); - useTagStore.getState().setTags(); } return { ok: response.ok, data: data.response }; @@ -243,7 +239,6 @@ const useLinkStore = create()((set) => ({ : e ), })); - useTagStore.getState().setTags(); } return { ok: response.ok, data: data.response }; @@ -262,7 +257,6 @@ const useLinkStore = create()((set) => ({ set((state) => ({ links: state.links.filter((e) => e.id !== linkId), })); - useTagStore.getState().setTags(); } return { ok: response.ok, data: data.response }; @@ -282,7 +276,6 @@ const useLinkStore = create()((set) => ({ set((state) => ({ links: state.links.filter((e) => !linkIds.includes(e.id as number)), })); - useTagStore.getState().setTags(); } return { ok: response.ok, data: data.response }; diff --git a/store/tags.ts b/store/tags.ts deleted file mode 100644 index 6778152..0000000 --- a/store/tags.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { create } from "zustand"; -import { TagIncludingLinkCount } from "@/types/global"; - -type ResponseObject = { - ok: boolean; - data: object | string; -}; - -type TagStore = { - tags: TagIncludingLinkCount[]; - setTags: () => void; - updateTag: (tag: TagIncludingLinkCount) => Promise; - removeTag: (tagId: number) => Promise; -}; - -const useTagStore = create()((set) => ({ - tags: [], - setTags: async () => { - const response = await fetch("/api/v1/tags"); - - const data = await response.json(); - - if (response.ok) set({ tags: data.response }); - }, - updateTag: async (tag) => { - const response = await fetch(`/api/v1/tags/${tag.id}`, { - body: JSON.stringify(tag), - headers: { - "Content-Type": "application/json", - }, - method: "PUT", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - tags: state.tags.map((e) => - e.id === data.response.id ? data.response : e - ), - })); - } - - return { ok: response.ok, data: data.response }; - }, - removeTag: async (tagId) => { - const response = await fetch(`/api/v1/tags/${tagId}`, { - method: "DELETE", - }); - - if (response.ok) { - set((state) => ({ - tags: state.tags.filter((e) => e.id !== tagId), - })); - } - - const data = await response.json(); - return { ok: response.ok, data: data.response }; - }, -})); - -export default useTagStore; From 8563a09a072ef4328fd69792391f812c7c527ede Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 1 Aug 2024 17:42:57 -0400 Subject: [PATCH 13/33] refactor token store --- components/ModalContent/NewTokenModal.tsx | 18 ++--- components/ModalContent/RevokeTokenModal.tsx | 23 ++---- hooks/store/tokens.tsx | 84 ++++++++++++++++++++ pages/settings/access-tokens.tsx | 14 +--- 4 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 hooks/store/tokens.tsx diff --git a/components/ModalContent/NewTokenModal.tsx b/components/ModalContent/NewTokenModal.tsx index 4d397e2..90e98a7 100644 --- a/components/ModalContent/NewTokenModal.tsx +++ b/components/ModalContent/NewTokenModal.tsx @@ -3,10 +3,10 @@ import TextInput from "@/components/TextInput"; import { TokenExpiry } from "@/types/global"; import toast from "react-hot-toast"; import Modal from "../Modal"; -import useTokenStore from "@/store/tokens"; import { dropdownTriggerer } from "@/lib/client/utils"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; +import { useAddToken } from "@/hooks/store/tokens"; type Props = { onClose: Function; @@ -15,7 +15,7 @@ type Props = { export default function NewTokenModal({ onClose }: Props) { const { t } = useTranslation(); const [newToken, setNewToken] = useState(""); - const { addToken } = useTokenStore(); + const addToken = useAddToken(); const initial = { name: "", @@ -28,16 +28,12 @@ export default function NewTokenModal({ onClose }: Props) { const submit = async () => { if (!submitLoader) { setSubmitLoader(true); - const load = toast.loading(t("creating_token")); - const { ok, data } = await addToken(token); - - toast.dismiss(load); - - if (ok) { - toast.success(t("token_created")); - setNewToken((data as any).secretKey); - } else toast.error(data as string); + await addToken.mutateAsync(token, { + onSuccess: (data) => { + setNewToken(data.secretKey); + }, + }); setSubmitLoader(false); } diff --git a/components/ModalContent/RevokeTokenModal.tsx b/components/ModalContent/RevokeTokenModal.tsx index 6b741e6..0e128c9 100644 --- a/components/ModalContent/RevokeTokenModal.tsx +++ b/components/ModalContent/RevokeTokenModal.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useState } from "react"; -import useTokenStore from "@/store/tokens"; -import toast from "react-hot-toast"; import Modal from "../Modal"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; import { AccessToken } from "@prisma/client"; +import { useRevokeToken } from "@/hooks/store/tokens"; type Props = { onClose: Function; @@ -15,26 +14,18 @@ export default function DeleteTokenModal({ onClose, activeToken }: Props) { const { t } = useTranslation(); const [token, setToken] = useState(activeToken); - const { revokeToken } = useTokenStore(); + const revokeToken = useRevokeToken(); useEffect(() => { setToken(activeToken); }, [activeToken]); const deleteLink = async () => { - const load = toast.loading(t("deleting")); - - const response = await revokeToken(token.id as number); - - toast.dismiss(load); - - if (response.ok) { - toast.success(t("token_revoked")); - } else { - toast.error(response.data as string); - } - - onClose(); + await revokeToken.mutateAsync(token.id, { + onSuccess: () => { + onClose(); + }, + }); }; return ( diff --git a/hooks/store/tokens.tsx b/hooks/store/tokens.tsx new file mode 100644 index 0000000..18bcd9d --- /dev/null +++ b/hooks/store/tokens.tsx @@ -0,0 +1,84 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; +import { AccessToken } from "@prisma/client"; + +const useTokens = () => { + return useQuery({ + queryKey: ["tokens"], + queryFn: async () => { + const response = await fetch("/api/v1/tokens"); + + if (!response.ok) throw new Error("Failed to fetch tokens."); + + const data = await response.json(); + return data.response as AccessToken[]; + }, + }); +}; + +const useAddToken = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (body: Partial) => { + const load = toast.loading(t("creating_token")); + + const response = await fetch("/api/v1/tokens", { + body: JSON.stringify(body), + method: "POST", + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data) => { + queryClient.setQueryData(["tokens"], (oldData: AccessToken[]) => [ + ...oldData, + data.token, + ]); + toast.success(t("token_added")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useRevokeToken = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (tokenId: number) => { + const load = toast.loading(t("deleting")); + + const response = await fetch(`/api/v1/tokens/${tokenId}`, { + method: "DELETE", + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.response); + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data, variables) => { + queryClient.setQueryData(["tokens"], (oldData: AccessToken[]) => + oldData.filter((token: Partial) => token.id !== variables) + ); + toast.success(t("token_revoked")); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { useTokens, useAddToken, useRevokeToken }; diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx index e526e2f..9e1cfd6 100644 --- a/pages/settings/access-tokens.tsx +++ b/pages/settings/access-tokens.tsx @@ -1,11 +1,11 @@ import SettingsLayout from "@/layouts/SettingsLayout"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import NewTokenModal from "@/components/ModalContent/NewTokenModal"; import RevokeTokenModal from "@/components/ModalContent/RevokeTokenModal"; import { AccessToken } from "@prisma/client"; -import useTokenStore from "@/store/tokens"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useTokens } from "@/hooks/store/tokens"; export default function AccessTokens() { const [newTokenModal, setNewTokenModal] = useState(false); @@ -18,15 +18,7 @@ export default function AccessTokens() { setRevokeTokenModal(true); }; - const { setTokens, tokens } = useTokenStore(); - - useEffect(() => { - fetch("/api/v1/tokens") - .then((res) => res.json()) - .then((data) => { - if (data.response) setTokens(data.response as AccessToken[]); - }); - }, []); + const { data: tokens = [] } = useTokens(); return ( From 75b1ae738fae69817cd0757b8414ac159b51155d Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 1 Aug 2024 17:43:46 -0400 Subject: [PATCH 14/33] remove unused code --- store/tokens.ts | 56 ------------------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 store/tokens.ts diff --git a/store/tokens.ts b/store/tokens.ts deleted file mode 100644 index eff1100..0000000 --- a/store/tokens.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { AccessToken } from "@prisma/client"; -import { create } from "zustand"; - -// Token store - -type ResponseObject = { - ok: boolean; - data: object | string; -}; - -type TokenStore = { - tokens: Partial[]; - setTokens: (data: Partial[]) => void; - addToken: (body: Partial[]) => Promise; - revokeToken: (tokenId: number) => Promise; -}; - -const useTokenStore = create((set) => ({ - tokens: [], - setTokens: async (data) => { - set(() => ({ - tokens: data, - })); - }, - addToken: async (body) => { - const response = await fetch("/api/v1/tokens", { - body: JSON.stringify(body), - method: "POST", - }); - - const data = await response.json(); - - if (response.ok) - set((state) => ({ - tokens: [...state.tokens, data.response.token], - })); - - return { ok: response.ok, data: data.response }; - }, - revokeToken: async (tokenId) => { - const response = await fetch(`/api/v1/tokens/${tokenId}`, { - method: "DELETE", - }); - - const data = await response.json(); - - if (response.ok) - set((state) => ({ - tokens: state.tokens.filter((token) => token.id !== tokenId), - })); - - return { ok: response.ok, data: data.response }; - }, -})); - -export default useTokenStore; From a73e5fa6c64176da3c44ad342cbf2ba0c98930f4 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 1 Aug 2024 18:40:08 -0400 Subject: [PATCH 15/33] add initialData to queries --- components/CollectionCard.tsx | 2 +- components/CollectionListing.tsx | 4 ++-- components/InputSelect/CollectionSelection.tsx | 2 +- components/InputSelect/TagSelection.tsx | 2 +- components/LinkViews/LinkCard.tsx | 4 ++-- components/LinkViews/LinkComponents/LinkActions.tsx | 2 +- components/LinkViews/LinkList.tsx | 4 ++-- components/LinkViews/LinkMasonry.tsx | 4 ++-- components/ModalContent/EditCollectionSharingModal.tsx | 2 +- components/ModalContent/NewLinkModal.tsx | 2 +- components/ModalContent/PreservedFormatsModal.tsx | 2 +- components/ModalContent/UploadFileModal.tsx | 2 +- components/ProfileDropdown.tsx | 2 +- components/ReadableView.tsx | 2 +- components/Sidebar.tsx | 4 ++-- hooks/store/collections.tsx | 1 + hooks/store/tags.tsx | 1 + hooks/store/tokens.tsx | 1 + hooks/store/user.tsx | 1 + hooks/useCollectivePermissions.ts | 4 ++-- hooks/useInitialData.tsx | 2 +- hooks/usePermissions.tsx | 4 ++-- layouts/AuthRedirect.tsx | 2 +- pages/admin.tsx | 2 +- pages/collections/[id].tsx | 4 ++-- pages/collections/index.tsx | 2 +- pages/dashboard.tsx | 4 ++-- pages/links/index.tsx | 3 +++ pages/public/collections/[id].tsx | 2 +- pages/settings/access-tokens.tsx | 2 +- pages/settings/account.tsx | 2 +- pages/settings/password.tsx | 2 +- pages/settings/preference.tsx | 2 +- pages/subscribe.tsx | 2 +- pages/tags/[id].tsx | 2 +- 35 files changed, 46 insertions(+), 39 deletions(-) diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index bb3a3dd..9a47f6e 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -20,7 +20,7 @@ type Props = { export default function CollectionCard({ collection, className }: Props) { const { t } = useTranslation(); const { settings } = useLocalSettingsStore(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const formattedDate = new Date(collection.createdAt as string).toLocaleString( "en-US", diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index 75b12cc..df85572 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -25,9 +25,9 @@ interface ExtendedTreeItem extends TreeItem { const CollectionListing = () => { const { t } = useTranslation(); const updateCollection = useUpdateCollection(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const updateUser = useUpdateUser(); const router = useRouter(); diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 81bef39..24700c2 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -24,7 +24,7 @@ export default function CollectionSelection({ showDefaultValue = true, creatable = true, }: Props) { - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const router = useRouter(); diff --git a/components/InputSelect/TagSelection.tsx b/components/InputSelect/TagSelection.tsx index efd246b..c9d3c27 100644 --- a/components/InputSelect/TagSelection.tsx +++ b/components/InputSelect/TagSelection.tsx @@ -13,7 +13,7 @@ type Props = { }; export default function TagSelection({ onChange, defaultValue }: Props) { - const { data: tags = [] } = useTags(); + const { data: tags } = useTags(); const [options, setOptions] = useState([]); diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx index e28ee9c..d6df2b3 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkCard.tsx @@ -34,9 +34,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); const viewMode = localStorage.getItem("viewMode") || "card"; - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index 0a3775e..430577b 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -39,7 +39,7 @@ export default function LinkActions({ const [deleteLinkModal, setDeleteLinkModal] = useState(false); const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const { removeLink, updateLink } = useLinkStore(); diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index 9755ad9..5711838 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -33,9 +33,9 @@ export default function LinkCardCompact({ }: Props) { const { t } = useTranslation(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const { links, setSelectedLinks, selectedLinks } = useLinkStore(); useEffect(() => { diff --git a/components/LinkViews/LinkMasonry.tsx b/components/LinkViews/LinkMasonry.tsx index ca05f1a..35cbaa9 100644 --- a/components/LinkViews/LinkMasonry.tsx +++ b/components/LinkViews/LinkMasonry.tsx @@ -33,8 +33,8 @@ type Props = { export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { const { t } = useTranslation(); - const { data: collections = [] } = useCollections(); - const { data: user = [] } = useUser(); + const { data: collections } = useCollections(); + const { data: user } = useUser(); const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore(); diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index 158f817..b1af6a4 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -46,7 +46,7 @@ export default function EditCollectionSharingModal({ } }; - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const permissions = usePermissions(collection.id as number); const currentURL = new URL(document.URL); diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 5164789..16c8489 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -43,7 +43,7 @@ export default function NewLinkModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 30bd7b6..3162013 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -30,7 +30,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { const { t } = useTranslation(); const session = useSession(); const { getLink } = useLinkStore(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const [link, setLink] = useState(activeLink); const router = useRouter(); diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 8cd2807..74a8d90 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -49,7 +49,7 @@ export default function UploadFileModal({ onClose }: Props) { const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index 2f262c2..4927517 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -9,7 +9,7 @@ import { useUser } from "@/hooks/store/user"; export default function ProfileDropdown() { const { t } = useTranslation(); const { settings, updateSettings } = useLocalSettingsStore(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1); diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index 70e21fe..da73b01 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -46,7 +46,7 @@ export default function ReadableView({ link }: Props) { const router = useRouter(); const { getLink } = useLinkStore(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const collection = useMemo(() => { return collections.find( diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 4348571..540d067 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -22,9 +22,9 @@ export default function Sidebar({ className }: { className?: string }) { } ); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); - const { data: tags = [] } = useTags(); + const { data: tags } = useTags(); const [active, setActive] = useState(""); const router = useRouter(); diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx index fe79d1e..932bb51 100644 --- a/hooks/store/collections.tsx +++ b/hooks/store/collections.tsx @@ -11,6 +11,7 @@ const useCollections = () => { const data = await response.json(); return data.response; }, + initialData: [], }); }; diff --git a/hooks/store/tags.tsx b/hooks/store/tags.tsx index c9738fe..df51829 100644 --- a/hooks/store/tags.tsx +++ b/hooks/store/tags.tsx @@ -13,6 +13,7 @@ const useTags = () => { const data = await response.json(); return data.response; }, + initialData: [], }); }; diff --git a/hooks/store/tokens.tsx b/hooks/store/tokens.tsx index 18bcd9d..20bef18 100644 --- a/hooks/store/tokens.tsx +++ b/hooks/store/tokens.tsx @@ -14,6 +14,7 @@ const useTokens = () => { const data = await response.json(); return data.response as AccessToken[]; }, + initialData: [], }); }; diff --git a/hooks/store/user.tsx b/hooks/store/user.tsx index 881cf0a..ca1c433 100644 --- a/hooks/store/user.tsx +++ b/hooks/store/user.tsx @@ -19,6 +19,7 @@ const useUser = () => { return data.response; }, enabled: !!userId, + initialData: {}, }); }; diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts index e093b64..b79cd4d 100644 --- a/hooks/useCollectivePermissions.ts +++ b/hooks/useCollectivePermissions.ts @@ -4,9 +4,9 @@ import { useCollections } from "./store/collections"; import { useUser } from "./store/user"; export default function useCollectivePermissions(collectionIds: number[]) { - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index fab5241..73e0096 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -6,7 +6,7 @@ import { useUser } from "./store/user"; export default function useInitialData() { const { status, data } = useSession(); // const { setLinks } = useLinkStore(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const { setSettings } = useLocalSettingsStore(); useEffect(() => { diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index 7b6725a..7625b49 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -4,9 +4,9 @@ import { useCollections } from "./store/collections"; import { useUser } from "./store/user"; export default function usePermissions(collectionId: number) { - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 821065a..0d07aee 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -14,7 +14,7 @@ export default function AuthRedirect({ children }: Props) { const router = useRouter(); const { status } = useSession(); const [shouldRenderChildren, setShouldRenderChildren] = useState(false); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); useInitialData(); diff --git a/pages/admin.tsx b/pages/admin.tsx index df62ffe..72d6f2b 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -21,7 +21,7 @@ type UserModal = { export default function Admin() { const { t } = useTranslation(); - const { data: users = [] } = useUsers(); + const { data: users } = useUsers(); const [searchQuery, setSearchQuery] = useState(""); const [filteredUsers, setFilteredUsers] = useState(); diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index 0c7ad8e..f226d2d 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -34,7 +34,7 @@ export default function Index() { const router = useRouter(); const { links } = useLinkStore(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); @@ -51,7 +51,7 @@ export default function Index() { ); }, [router, collections]); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); const [collectionOwner, setCollectionOwner] = useState({ id: null as unknown as number, diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index f57ddfc..0bf2c7e 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -13,7 +13,7 @@ import { useCollections } from "@/hooks/store/collections"; export default function Collections() { const { t } = useTranslation(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); const [sortedCollections, setSortedCollections] = useState(collections); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index adfa163..a609c6b 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -22,9 +22,9 @@ import { useTags } from "@/hooks/store/tags"; export default function Dashboard() { const { t } = useTranslation(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const { links } = useLinkStore(); - const { data: tags = [] } = useTags(); + const { data: tags } = useTags(); const [numberOfLinks, setNumberOfLinks] = useState(0); diff --git a/pages/links/index.tsx b/pages/links/index.tsx index 35c1488..1580324 100644 --- a/pages/links/index.tsx +++ b/pages/links/index.tsx @@ -1,5 +1,6 @@ import NoLinksFound from "@/components/NoLinksFound"; import useLinks from "@/hooks/useLinks"; +// import { useLinks } from "@/hooks/store/links"; import MainLayout from "@/layouts/MainLayout"; import useLinkStore from "@/store/links"; import React, { useEffect, useState } from "react"; @@ -15,6 +16,8 @@ import { useTranslation } from "next-i18next"; export default function Links() { const { t } = useTranslation(); + + // const { data: links } = useLinks(); const { links } = useLinkStore(); const [viewMode, setViewMode] = useState( diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index db06ec8..632b831 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -32,7 +32,7 @@ export default function PublicCollections() { const { settings } = useLocalSettingsStore(); - const { data: collections = [] } = useCollections(); + const { data: collections } = useCollections(); const router = useRouter(); diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx index 9e1cfd6..91d1761 100644 --- a/pages/settings/access-tokens.tsx +++ b/pages/settings/access-tokens.tsx @@ -18,7 +18,7 @@ export default function AccessTokens() { setRevokeTokenModal(true); }; - const { data: tokens = [] } = useTokens(); + const { data: tokens } = useTokens(); return ( diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 5c4b109..c392596 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -24,7 +24,7 @@ export default function Account() { const [emailChangeVerificationModal, setEmailChangeVerificationModal] = useState(false); const [submitLoader, setSubmitLoader] = useState(false); - const { data: account = [] } = useUser(); + const { data: account } = useUser(); const updateUser = useUpdateUser(); const [user, setUser] = useState( !objectIsEmpty(account) diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx index d63daf4..4b0d3e8 100644 --- a/pages/settings/password.tsx +++ b/pages/settings/password.tsx @@ -13,7 +13,7 @@ export default function Password() { const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [submitLoader, setSubmitLoader] = useState(false); - const { data: account = [] } = useUser(); + const { data: account } = useUser(); const updateUser = useUpdateUser(); const submit = async () => { diff --git a/pages/settings/preference.tsx b/pages/settings/preference.tsx index 18baee7..2e33ca1 100644 --- a/pages/settings/preference.tsx +++ b/pages/settings/preference.tsx @@ -13,7 +13,7 @@ export default function Appearance() { const { t } = useTranslation(); const { updateSettings } = useLocalSettingsStore(); const [submitLoader, setSubmitLoader] = useState(false); - const { data: account = [] } = useUser(); + const { data: account } = useUser(); const updateUser = useUpdateUser(); const [user, setUser] = useState(account); diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index fb2ad3a..fe8bfd2 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -20,7 +20,7 @@ export default function Subscribe() { const router = useRouter(); - const { data: user = [] } = useUser(); + const { data: user } = useUser(); useEffect(() => { const hasInactiveSubscription = diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 2467da6..92fffe7 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -21,7 +21,7 @@ export default function Index() { const router = useRouter(); const { links } = useLinkStore(); - const { data: tags = [] } = useTags(); + const { data: tags } = useTags(); const updateTag = useUpdateTag(); const removeTag = useRemoveTag(); From 102690fc10dbcd5a6d87c2c76fcab974d6cf3153 Mon Sep 17 00:00:00 2001 From: jlssmt Date: Thu, 25 Jul 2024 22:50:38 +0200 Subject: [PATCH 16/33] handle undefined --- lib/api/controllers/links/postLink.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 8056c11..c2105e3 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -86,6 +86,8 @@ export default async function postLink( else if (contentType === "image/png") imageExtension = "png"; } + if (!link.tags) link.tags = []; + const newLink = await prisma.link.create({ data: { url: link.url?.trim().replace(/\/+$/, "") || null, @@ -98,7 +100,7 @@ export default async function postLink( }, }, tags: { - connectOrCreate: link.tags.map((tag) => ({ + connectOrCreate: link.tags?.map((tag) => ({ where: { name_ownerId: { name: tag.name.trim(), From 0158e58d9036a51fc465b94647b23b4c681fdb84 Mon Sep 17 00:00:00 2001 From: phillibl Date: Wed, 7 Aug 2024 05:29:10 -0400 Subject: [PATCH 17/33] Update [...nextauth].ts Fixed issue where sign in would fail for existing user if DISABLE_NEW_SSO_USERS = true --- pages/api/v1/auth/[...nextauth].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/v1/auth/[...nextauth].ts b/pages/api/v1/auth/[...nextauth].ts index 17bcaad..7b6315f 100644 --- a/pages/api/v1/auth/[...nextauth].ts +++ b/pages/api/v1/auth/[...nextauth].ts @@ -1186,7 +1186,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) { providerAccountId: account?.providerAccountId, }, }); - if (existingUser && newSsoUsersDisabled) { + if (!existingUser && newSsoUsersDisabled) { return false; } } From 80f366cd7beeae748e594d5f87ba3c3b977cb1e3 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 13 Aug 2024 00:08:57 -0400 Subject: [PATCH 18/33] refactored link state management + a lot of other changes... --- components/CollectionCard.tsx | 2 +- components/CollectionListing.tsx | 4 +- .../InputSelect/CollectionSelection.tsx | 2 +- components/InputSelect/TagSelection.tsx | 2 +- components/LinkListOptions.tsx | 123 ++--- components/LinkViews/Layouts/CardView.tsx | 23 +- components/LinkViews/LinkCard.tsx | 15 +- .../LinkViews/LinkComponents/LinkActions.tsx | 40 +- .../LinkComponents/LinkCollection.tsx | 33 +- components/LinkViews/LinkList.tsx | 9 +- components/LinkViews/LinkMasonry.tsx | 12 +- components/LinkViews/Links.tsx | 226 ++++++++ .../ModalContent/BulkDeleteLinksModal.tsx | 26 +- components/ModalContent/DeleteLinkModal.tsx | 34 +- .../EditCollectionSharingModal.tsx | 2 +- components/ModalContent/EditLinkModal.tsx | 21 +- components/ModalContent/NewLinkModal.tsx | 25 +- .../ModalContent/PreservedFormatsModal.tsx | 12 +- components/ModalContent/UploadFileModal.tsx | 25 +- components/PreserverdFormatRow.tsx | 11 +- components/ProfileDropdown.tsx | 2 +- components/ReadableView.tsx | 13 +- components/Sidebar.tsx | 2 +- components/SortDropdown.tsx | 9 +- components/ViewDropdown.tsx | 6 +- hooks/store/collections.tsx | 3 - hooks/store/dashboardData.tsx | 16 + hooks/store/links.tsx | 499 ++++++++++++++++++ hooks/store/publicLinks.tsx | 93 ++++ hooks/store/tags.tsx | 1 - hooks/store/tokens.tsx | 1 - hooks/store/user.tsx | 2 +- hooks/useCollectivePermissions.ts | 4 +- hooks/useInitialData.tsx | 2 +- hooks/useLinks.tsx | 103 ---- hooks/usePermissions.tsx | 4 +- layouts/AuthRedirect.tsx | 2 +- .../controllers/dashboard/getDashboardData.ts | 16 +- lib/api/controllers/links/getLinks.ts | 4 +- package.json | 1 + pages/_app.tsx | 17 +- pages/admin.tsx | 2 +- pages/collections/[id].tsx | 54 +- pages/collections/index.tsx | 6 +- pages/dashboard.tsx | 70 ++- pages/links/index.tsx | 47 +- pages/links/pinned.tsx | 46 +- pages/preserved/[id].tsx | 11 +- pages/public/collections/[id].tsx | 59 +-- pages/public/preserved/[id].tsx | 12 +- pages/search.tsx | 53 +- pages/settings/access-tokens.tsx | 2 +- pages/subscribe.tsx | 2 +- pages/tags/[id].tsx | 42 +- store/links.ts | 246 +-------- store/localSettings.ts | 14 +- types/global.ts | 3 +- yarn.lock | 5 + 58 files changed, 1302 insertions(+), 819 deletions(-) create mode 100644 components/LinkViews/Links.tsx create mode 100644 hooks/store/dashboardData.tsx create mode 100644 hooks/store/links.tsx create mode 100644 hooks/store/publicLinks.tsx delete mode 100644 hooks/useLinks.tsx diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index 9a47f6e..aaf177e 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -20,7 +20,7 @@ type Props = { export default function CollectionCard({ collection, className }: Props) { const { t } = useTranslation(); const { settings } = useLocalSettingsStore(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const formattedDate = new Date(collection.createdAt as string).toLocaleString( "en-US", diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index df85572..f118184 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -25,9 +25,9 @@ interface ExtendedTreeItem extends TreeItem { const CollectionListing = () => { const { t } = useTranslation(); const updateCollection = useUpdateCollection(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const updateUser = useUpdateUser(); const router = useRouter(); diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 24700c2..81bef39 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -24,7 +24,7 @@ export default function CollectionSelection({ showDefaultValue = true, creatable = true, }: Props) { - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); diff --git a/components/InputSelect/TagSelection.tsx b/components/InputSelect/TagSelection.tsx index c9d3c27..efd246b 100644 --- a/components/InputSelect/TagSelection.tsx +++ b/components/InputSelect/TagSelection.tsx @@ -13,7 +13,7 @@ type Props = { }; export default function TagSelection({ onChange, defaultValue }: Props) { - const { data: tags } = useTags(); + const { data: tags = [] } = useTags(); const [options, setOptions] = useState([]); diff --git a/components/LinkListOptions.tsx b/components/LinkListOptions.tsx index 842363e..6ec063d 100644 --- a/components/LinkListOptions.tsx +++ b/components/LinkListOptions.tsx @@ -5,17 +5,17 @@ import ViewDropdown from "./ViewDropdown"; import { TFunction } from "i18next"; import BulkDeleteLinksModal from "./ModalContent/BulkDeleteLinksModal"; import BulkEditLinksModal from "./ModalContent/BulkEditLinksModal"; -import toast from "react-hot-toast"; import useCollectivePermissions from "@/hooks/useCollectivePermissions"; import { useRouter } from "next/router"; import useLinkStore from "@/store/links"; -import { Sort } from "@/types/global"; +import { Sort, ViewMode } from "@/types/global"; +import { useBulkDeleteLinks, useLinks } from "@/hooks/store/links"; type Props = { children: React.ReactNode; t: TFunction<"translation", undefined>; - viewMode: string; - setViewMode: Dispatch>; + viewMode: ViewMode; + setViewMode: Dispatch>; searchFilter?: { name: boolean; url: boolean; @@ -48,8 +48,11 @@ const LinkListOptions = ({ editMode, setEditMode, }: Props) => { - const { links, selectedLinks, setSelectedLinks, deleteLinksById } = - useLinkStore(); + const { selectedLinks, setSelectedLinks } = useLinkStore(); + + const deleteLinksById = useBulkDeleteLinks(); + + const { links } = useLinks(); const router = useRouter(); @@ -73,23 +76,14 @@ const LinkListOptions = ({ }; const bulkDeleteLinks = async () => { - const load = toast.loading(t("deleting_selections")); - - const response = await deleteLinksById( - selectedLinks.map((link) => link.id as number) + await deleteLinksById.mutateAsync( + selectedLinks.map((link) => link.id as number), + { + onSuccess: () => { + setSelectedLinks([]); + }, + } ); - - toast.dismiss(load); - - if (response.ok) { - toast.success( - selectedLinks.length === 1 - ? t("link_deleted") - : t("links_deleted", { count: selectedLinks.length }) - ); - } else { - toast.error(response.data as string); - } }; return ( @@ -99,57 +93,64 @@ const LinkListOptions = ({
- {links.length > 0 && editMode !== undefined && setEditMode && ( -
{ - setEditMode(!editMode); - setSelectedLinks([]); - }} - className={`btn btn-square btn-sm btn-ghost ${ - editMode - ? "bg-primary/20 hover:bg-primary/20" - : "hover:bg-neutral/20" - }`} - > - -
- )} + {links && + links.length > 0 && + editMode !== undefined && + setEditMode && ( +
{ + setEditMode(!editMode); + setSelectedLinks([]); + }} + className={`btn btn-square btn-sm btn-ghost ${ + editMode + ? "bg-primary/20 hover:bg-primary/20" + : "hover:bg-neutral/20" + }`} + > + +
+ )} {searchFilter && setSearchFilter && ( )} - + { + setSortBy(value); + }} + t={t} + />
- {editMode && links.length > 0 && ( + {links && editMode && links.length > 0 && (
- {links.length > 0 && ( -
- handleSelectAll()} - checked={ - selectedLinks.length === links.length && links.length > 0 - } - /> - {selectedLinks.length > 0 ? ( - - {selectedLinks.length === 1 - ? t("link_selected") - : t("links_selected", { count: selectedLinks.length })} - - ) : ( - {t("nothing_selected")} - )} -
- )} +
+ handleSelectAll()} + checked={ + selectedLinks.length === links.length && links.length > 0 + } + /> + {selectedLinks.length > 0 ? ( + + {selectedLinks.length === 1 + ? t("link_selected") + : t("links_selected", { count: selectedLinks.length })} + + ) : ( + {t("nothing_selected")} + )} +
diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index b1af6a4..ae52ef8 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -46,7 +46,7 @@ export default function EditCollectionSharingModal({ } }; - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const permissions = usePermissions(collection.id as number); const currentURL = new URL(document.URL); diff --git a/components/ModalContent/EditLinkModal.tsx b/components/ModalContent/EditLinkModal.tsx index 1d873c7..7613c25 100644 --- a/components/ModalContent/EditLinkModal.tsx +++ b/components/ModalContent/EditLinkModal.tsx @@ -3,12 +3,11 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; -import toast from "react-hot-toast"; import Link from "next/link"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; +import { useUpdateLink } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -27,9 +26,10 @@ export default function EditLinkModal({ onClose, activeLink }: Props) { console.log(error); } - const { updateLink } = useLinkStore(); const [submitLoader, setSubmitLoader] = useState(false); + const updateLink = useUpdateLink(); + const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; setLink({ @@ -50,19 +50,14 @@ export default function EditLinkModal({ onClose, activeLink }: Props) { const submit = async () => { if (!submitLoader) { setSubmitLoader(true); - const load = toast.loading(t("updating")); - let response = await updateLink(link); - toast.dismiss(load); - if (response.ok) { - toast.success(t("updated")); - onClose(); - } else { - toast.error(response.data as string); - } + await updateLink.mutateAsync(link, { + onSuccess: () => { + onClose(); + }, + }); setSubmitLoader(false); - return response; } }; diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 16c8489..05350a5 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -3,14 +3,13 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { useSession } from "next-auth/react"; import { useRouter } from "next/router"; -import toast from "react-hot-toast"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useAddLink } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -39,11 +38,13 @@ export default function NewLinkModal({ onClose }: Props) { const [link, setLink] = useState(initial); - const { addLink } = useLinkStore(); + + const addLink = useAddLink(); + const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; @@ -86,15 +87,13 @@ export default function NewLinkModal({ onClose }: Props) { const submit = async () => { if (!submitLoader) { setSubmitLoader(true); - const load = toast.loading(t("creating_link")); - const response = await addLink(link); - toast.dismiss(load); - if (response.ok) { - toast.success(t("link_created")); - onClose(); - } else { - toast.error(response.data as string); - } + + await addLink.mutateAsync(link, { + onSuccess: () => { + onClose(); + }, + }); + setSubmitLoader(false); } }; diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 3162013..3d2e29d 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags, ArchivedFormat, @@ -20,6 +19,7 @@ import getPublicUserData from "@/lib/client/getPublicUserData"; import { useTranslation } from "next-i18next"; import { BeatLoader } from "react-spinners"; import { useUser } from "@/hooks/store/user"; +import { useGetLink } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -29,8 +29,8 @@ type Props = { export default function PreservedFormatsModal({ onClose, activeLink }: Props) { const { t } = useTranslation(); const session = useSession(); - const { getLink } = useLinkStore(); - const { data: user } = useUser(); + const getLink = useGetLink(); + const { data: user = {} } = useUser(); const [link, setLink] = useState(activeLink); const router = useRouter(); @@ -98,7 +98,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { useEffect(() => { (async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -108,7 +108,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { if (!isReady()) { interval = setInterval(async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -137,7 +137,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { toast.dismiss(load); if (response.ok) { - const newLink = await getLink(link?.id as number); + const newLink = await getLink.mutateAsync(link?.id as number); setLink( (newLink as any).response as LinkIncludingShortenedCollectionAndTags ); diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 74a8d90..2e43d56 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -3,7 +3,6 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import TagSelection from "@/components/InputSelect/TagSelection"; import TextInput from "@/components/TextInput"; import unescapeString from "@/lib/client/unescapeString"; -import useLinkStore from "@/store/links"; import { LinkIncludingShortenedCollectionAndTags, ArchivedFormat, @@ -14,6 +13,7 @@ import toast from "react-hot-toast"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useUploadFile } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -45,11 +45,11 @@ export default function UploadFileModal({ onClose }: Props) { useState(initial); const [file, setFile] = useState(); - const { uploadFile } = useLinkStore(); + const uploadFile = useUploadFile(); const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); const router = useRouter(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const setCollection = (e: any) => { if (e?.__isNew__) e.value = null; @@ -115,20 +115,17 @@ export default function UploadFileModal({ onClose }: Props) { // } setSubmitLoader(true); - const load = toast.loading(t("creating")); - const response = await uploadFile(link, file); - - toast.dismiss(load); - if (response.ok) { - toast.success(t("created_success")); - onClose(); - } else { - toast.error(response.data as string); - } + await uploadFile.mutateAsync( + { link, file }, + { + onSuccess: () => { + onClose(); + }, + } + ); setSubmitLoader(false); - return response; } }; diff --git a/components/PreserverdFormatRow.tsx b/components/PreserverdFormatRow.tsx index ce0f21d..2e18e39 100644 --- a/components/PreserverdFormatRow.tsx +++ b/components/PreserverdFormatRow.tsx @@ -1,13 +1,11 @@ import React, { useEffect, useState } from "react"; -import useLinkStore from "@/store/links"; import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; -import toast from "react-hot-toast"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useSession } from "next-auth/react"; +import { useGetLink } from "@/hooks/store/links"; type Props = { name: string; @@ -24,8 +22,7 @@ export default function PreservedFormatRow({ activeLink, downloadable, }: Props) { - const session = useSession(); - const { getLink } = useLinkStore(); + const getLink = useGetLink(); const [link, setLink] = useState(activeLink); @@ -36,7 +33,7 @@ export default function PreservedFormatRow({ useEffect(() => { (async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); @@ -45,7 +42,7 @@ export default function PreservedFormatRow({ let interval: any; if (link?.image === "pending" || link?.pdf === "pending") { interval = setInterval(async () => { - const data = await getLink(link.id as number, isPublic); + const data = await getLink.mutateAsync(link.id as number); setLink( (data as any).response as LinkIncludingShortenedCollectionAndTags ); diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index 4927517..d32893d 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -9,7 +9,7 @@ import { useUser } from "@/hooks/store/user"; export default function ProfileDropdown() { const { t } = useTranslation(); const { settings, updateSettings } = useLocalSettingsStore(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1); diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index da73b01..801e9c1 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -1,7 +1,6 @@ import unescapeString from "@/lib/client/unescapeString"; import { readabilityAvailable } from "@/lib/shared/getArchiveValidity"; import isValidUrl from "@/lib/shared/isValidUrl"; -import useLinkStore from "@/store/links"; import { ArchivedFormat, CollectionIncludingMembersAndLinkCount, @@ -16,6 +15,7 @@ import React, { useEffect, useMemo, useState } from "react"; import LinkActions from "./LinkViews/LinkComponents/LinkActions"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; +import { useGetLink } from "@/hooks/store/links"; type LinkContent = { title: string; @@ -45,8 +45,8 @@ export default function ReadableView({ link }: Props) { const router = useRouter(); - const { getLink } = useLinkStore(); - const { data: collections } = useCollections(); + const getLink = useGetLink(); + const { data: collections = [] } = useCollections(); const collection = useMemo(() => { return collections.find( @@ -73,7 +73,7 @@ export default function ReadableView({ link }: Props) { }, [link]); useEffect(() => { - if (link) getLink(link?.id as number); + if (link) getLink.mutateAsync(link?.id as number); let interval: any; if ( @@ -87,7 +87,10 @@ export default function ReadableView({ link }: Props) { !link?.readable || !link?.monolith) ) { - interval = setInterval(() => getLink(link.id as number), 5000); + interval = setInterval( + () => getLink.mutateAsync(link.id as number), + 5000 + ); } else { if (interval) { clearInterval(interval); diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 540d067..dc15c61 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -24,7 +24,7 @@ export default function Sidebar({ className }: { className?: string }) { const { data: collections } = useCollections(); - const { data: tags } = useTags(); + const { data: tags = [] } = useTags(); const [active, setActive] = useState(""); const router = useRouter(); diff --git a/components/SortDropdown.tsx b/components/SortDropdown.tsx index 002dac5..809d4c3 100644 --- a/components/SortDropdown.tsx +++ b/components/SortDropdown.tsx @@ -1,7 +1,8 @@ -import React, { Dispatch, SetStateAction } from "react"; +import React, { Dispatch, SetStateAction, useEffect } from "react"; import { Sort } from "@/types/global"; import { dropdownTriggerer } from "@/lib/client/utils"; import { TFunction } from "i18next"; +import useLocalSettingsStore from "@/store/localSettings"; type Props = { sortBy: Sort; @@ -10,6 +11,12 @@ type Props = { }; export default function SortDropdown({ sortBy, setSort, t }: Props) { + const { updateSettings } = useLocalSettingsStore(); + + useEffect(() => { + updateSettings({ sortBy }); + }, [sortBy]); + return (
>; + viewMode: ViewMode; + setViewMode: Dispatch>; }; export default function ViewDropdown({ viewMode, setViewMode }: Props) { @@ -19,7 +19,7 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) { }; useEffect(() => { - updateSettings({ viewMode: viewMode as ViewMode }); + updateSettings({ viewMode }); }, [viewMode]); return ( diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx index 932bb51..4d0a93f 100644 --- a/hooks/store/collections.tsx +++ b/hooks/store/collections.tsx @@ -11,7 +11,6 @@ const useCollections = () => { const data = await response.json(); return data.response; }, - initialData: [], }); }; @@ -42,8 +41,6 @@ const useCreateCollection = () => { onSuccess: (data) => { toast.success(t("created")); return queryClient.setQueryData(["collections"], (oldData: any) => { - console.log([...oldData, data]); - return [...oldData, data]; }); }, diff --git a/hooks/store/dashboardData.tsx b/hooks/store/dashboardData.tsx new file mode 100644 index 0000000..358b035 --- /dev/null +++ b/hooks/store/dashboardData.tsx @@ -0,0 +1,16 @@ +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import { useQuery } from "@tanstack/react-query"; + +const useDashboardData = () => { + return useQuery({ + queryKey: ["dashboardData"], + queryFn: async (): Promise => { + const response = await fetch("/api/v1/dashboard"); + const data = await response.json(); + + return data.response; + }, + }); +}; + +export { useDashboardData }; diff --git a/hooks/store/links.tsx b/hooks/store/links.tsx new file mode 100644 index 0000000..93b9b75 --- /dev/null +++ b/hooks/store/links.tsx @@ -0,0 +1,499 @@ +import { + InfiniteData, + useInfiniteQuery, + UseInfiniteQueryResult, + useQueryClient, + useMutation, +} from "@tanstack/react-query"; +import { useEffect, useMemo } from "react"; +import { + ArchivedFormat, + LinkIncludingShortenedCollectionAndTags, + LinkRequestQuery, +} from "@/types/global"; +import toast from "react-hot-toast"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; + +const useLinks = (params: LinkRequestQuery = {}) => { + const router = useRouter(); + + const queryParamsObject = { + sort: params.sort ?? Number(window.localStorage.getItem("sortBy")) ?? 0, + collectionId: + params.collectionId ?? router.pathname === "/collections/[id]" + ? router.query.id + : undefined, + tagId: + params.tagId ?? router.pathname === "/tags/[id]" + ? router.query.id + : undefined, + pinnedOnly: + params.pinnedOnly ?? router.pathname === "/links/pinned" + ? true + : undefined, + searchQueryString: params.searchQueryString, + searchByName: params.searchByName, + searchByUrl: params.searchByUrl, + searchByDescription: params.searchByDescription, + searchByTextContent: params.searchByTextContent, + searchByTags: params.searchByTags, + } as LinkRequestQuery; + + const queryString = buildQueryString(queryParamsObject); + + const { data, ...rest } = useFetchLinks(queryString); + + const links = useMemo(() => { + return data?.pages.reduce((acc, page) => { + return [...acc, ...page]; + }, []); + }, [data]); + + return { + links, + data: { ...data, ...rest }, + } as { + links: LinkIncludingShortenedCollectionAndTags[]; + data: UseInfiniteQueryResult, Error>; + }; +}; + +const useFetchLinks = (params: string) => { + return useInfiniteQuery({ + queryKey: ["links", { params }], + queryFn: async (params) => { + const response = await fetch( + "/api/v1/links?cursor=" + + params.pageParam + + ((params.queryKey[1] as any).params + ? "&" + (params.queryKey[1] as any).params + : "") + ); + const data = await response.json(); + + return data.response; + }, + initialPageParam: 0, + refetchOnWindowFocus: false, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPage.at(-1).id; + }, + }); +}; + +const buildQueryString = (params: LinkRequestQuery) => { + return Object.keys(params) + .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + params[key as keyof LinkRequestQuery] as string + )}` + ) + .join("&"); +}; + +const useAddLink = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => { + const load = toast.loading(t("creating_link")); + + const response = await fetch("/api/v1/links", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(link), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("link_created")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return [data, ...oldData]; + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)], + pageParams: oldData?.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useUpdateLink = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => { + const load = toast.loading(t("updating")); + + const response = await fetch(`/api/v1/links/${link.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(link), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("updated")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.map((e: any) => (e.id === data.id ? data : e)); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useDeleteLink = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id: number) => { + const load = toast.loading(t("deleting")); + + const response = await fetch(`/api/v1/links/${id}`, { + method: "DELETE", + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("deleted")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.filter((e: any) => e.id !== data.id); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].filter((e: any) => e.id !== data.id), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useGetLink = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id: number) => { + const response = await fetch(`/api/v1/links/${id}`); + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.map((e: any) => (e.id === data.id ? data : e)); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + }); +}; + +const useBulkDeleteLinks = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (linkIds: number[]) => { + const load = toast.loading(t("deleting")); + + const response = await fetch("/api/v1/links", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ linkIds }), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return linkIds; + }, + onSuccess: (data) => { + toast.success(t("deleted")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.filter((e: any) => !data.includes(e.id)); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].filter((e: any) => !data.includes(e.id)), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useUploadFile = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ link, file }: any) => { + let fileType: ArchivedFormat | null = null; + let linkType: "url" | "image" | "pdf" | null = null; + + if (file?.type === "image/jpg" || file.type === "image/jpeg") { + fileType = ArchivedFormat.jpeg; + linkType = "image"; + } else if (file.type === "image/png") { + fileType = ArchivedFormat.png; + linkType = "image"; + } else if (file.type === "application/pdf") { + fileType = ArchivedFormat.pdf; + linkType = "pdf"; + } else { + return { ok: false, data: "Invalid file type." }; + } + + const load = toast.loading(t("creating")); + + const response = await fetch("/api/v1/links", { + body: JSON.stringify({ + ...link, + type: linkType, + name: link.name ? link.name : file.name, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + if (response.ok) { + const formBody = new FormData(); + file && formBody.append("file", file); + + await fetch( + `/api/v1/archives/${(data as any).response.id}?format=${fileType}`, + { + body: formBody, + method: "POST", + } + ); + } + + toast.dismiss(load); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("created_success")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return [data, ...oldData]; + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)], + pageParams: oldData?.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +const useBulkEditLinks = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + links, + newData, + removePreviousTags, + }: { + links: LinkIncludingShortenedCollectionAndTags[]; + newData: Pick< + LinkIncludingShortenedCollectionAndTags, + "tags" | "collectionId" + >; + removePreviousTags: boolean; + }) => { + const load = toast.loading(t("updating")); + + const response = await fetch("/api/v1/links", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ links, newData, removePreviousTags }), + }); + + toast.dismiss(load); + + const data = await response.json(); + + if (!response.ok) throw new Error(data.response); + + return data.response; + }, + onSuccess: (data) => { + toast.success(t("updated")); + + queryClient.setQueryData(["dashboardData"], (oldData: any) => { + if (!oldData) return undefined; + return oldData.map((e: any) => + data.find((d: any) => d.id === e.id) ? data : e + ); + }); + + queryClient.setQueryData(["links"], (oldData: any) => { + if (!oldData) return undefined; + return { + pages: [ + oldData.pages[0].map((e: any) => + data.find((d: any) => d.id === e.id) ? data : e + ), + ...oldData.pages.slice(1), + ], + pageParams: oldData.pageParams, + }; + }); + + queryClient.invalidateQueries({ queryKey: ["collections"] }); + queryClient.invalidateQueries({ queryKey: ["tags"] }); + queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + }, + onError: (error) => { + toast.error(error.message); + }, + }); +}; + +export { + useLinks, + useAddLink, + useUpdateLink, + useDeleteLink, + useBulkDeleteLinks, + useUploadFile, + useGetLink, + useBulkEditLinks, +}; diff --git a/hooks/store/publicLinks.tsx b/hooks/store/publicLinks.tsx new file mode 100644 index 0000000..86c7961 --- /dev/null +++ b/hooks/store/publicLinks.tsx @@ -0,0 +1,93 @@ +import { + InfiniteData, + useInfiniteQuery, + UseInfiniteQueryResult, +} from "@tanstack/react-query"; +import { useMemo } from "react"; +import { + LinkIncludingShortenedCollectionAndTags, + LinkRequestQuery, +} from "@/types/global"; +import { useRouter } from "next/router"; + +const usePublicLinks = (params: LinkRequestQuery = {}) => { + const router = useRouter(); + + const queryParamsObject = { + sort: params.sort ?? Number(window.localStorage.getItem("sortBy")) ?? 0, + collectionId: params.collectionId ?? router.query.id, + tagId: + params.tagId ?? router.pathname === "/tags/[id]" + ? router.query.id + : undefined, + pinnedOnly: + params.pinnedOnly ?? router.pathname === "/links/pinned" + ? true + : undefined, + searchQueryString: params.searchQueryString, + searchByName: params.searchByName, + searchByUrl: params.searchByUrl, + searchByDescription: params.searchByDescription, + searchByTextContent: params.searchByTextContent, + searchByTags: params.searchByTags, + } as LinkRequestQuery; + + const queryString = buildQueryString(queryParamsObject); + + const { data, ...rest } = useFetchLinks(queryString); + + const links = useMemo(() => { + return data?.pages.reduce((acc, page) => { + return [...acc, ...page]; + }, []); + }, [data]); + + return { + links, + data: { ...data, ...rest }, + } as { + links: LinkIncludingShortenedCollectionAndTags[]; + data: UseInfiniteQueryResult, Error>; + }; +}; + +const useFetchLinks = (params: string) => { + return useInfiniteQuery({ + queryKey: ["links", { params }], + queryFn: async (params) => { + const response = await fetch( + "/api/v1/public/collections/links?cursor=" + + params.pageParam + + ((params.queryKey[1] as any).params + ? "&" + (params.queryKey[1] as any).params + : "") + ); + + const data = await response.json(); + + return data.response; + }, + initialPageParam: 0, + refetchOnWindowFocus: false, + getNextPageParam: (lastPage) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPage.at(-1).id; + }, + }); +}; + +const buildQueryString = (params: LinkRequestQuery) => { + return Object.keys(params) + .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) + .map( + (key) => + `${encodeURIComponent(key)}=${encodeURIComponent( + params[key as keyof LinkRequestQuery] as string + )}` + ) + .join("&"); +}; + +export { usePublicLinks }; diff --git a/hooks/store/tags.tsx b/hooks/store/tags.tsx index df51829..c9738fe 100644 --- a/hooks/store/tags.tsx +++ b/hooks/store/tags.tsx @@ -13,7 +13,6 @@ const useTags = () => { const data = await response.json(); return data.response; }, - initialData: [], }); }; diff --git a/hooks/store/tokens.tsx b/hooks/store/tokens.tsx index 20bef18..18bcd9d 100644 --- a/hooks/store/tokens.tsx +++ b/hooks/store/tokens.tsx @@ -14,7 +14,6 @@ const useTokens = () => { const data = await response.json(); return data.response as AccessToken[]; }, - initialData: [], }); }; diff --git a/hooks/store/user.tsx b/hooks/store/user.tsx index ca1c433..a470cc5 100644 --- a/hooks/store/user.tsx +++ b/hooks/store/user.tsx @@ -19,7 +19,7 @@ const useUser = () => { return data.response; }, enabled: !!userId, - initialData: {}, + placeholderData: {}, }); }; diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts index b79cd4d..0d2e5fb 100644 --- a/hooks/useCollectivePermissions.ts +++ b/hooks/useCollectivePermissions.ts @@ -4,9 +4,9 @@ import { useCollections } from "./store/collections"; import { useUser } from "./store/user"; export default function useCollectivePermissions(collectionIds: number[]) { - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index 73e0096..04cf247 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -6,7 +6,7 @@ import { useUser } from "./store/user"; export default function useInitialData() { const { status, data } = useSession(); // const { setLinks } = useLinkStore(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const { setSettings } = useLocalSettingsStore(); useEffect(() => { diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx deleted file mode 100644 index bec2d49..0000000 --- a/hooks/useLinks.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { LinkRequestQuery } from "@/types/global"; -import { useEffect, useState } from "react"; -import useDetectPageBottom from "./useDetectPageBottom"; -import { useRouter } from "next/router"; -import useLinkStore from "@/store/links"; - -export default function useLinks( - { - sort, - collectionId, - tagId, - pinnedOnly, - searchQueryString, - searchByName, - searchByUrl, - searchByDescription, - searchByTags, - searchByTextContent, - }: LinkRequestQuery = { sort: 0 } -) { - const { links, setLinks, resetLinks, selectedLinks, setSelectedLinks } = - useLinkStore(); - const router = useRouter(); - - const [isLoading, setIsLoading] = useState(true); - - const { reachedBottom, setReachedBottom } = useDetectPageBottom(); - - const getLinks = async (isInitialCall: boolean, cursor?: number) => { - const params = { - sort, - cursor, - collectionId, - tagId, - pinnedOnly, - searchQueryString, - searchByName, - searchByUrl, - searchByDescription, - searchByTags, - searchByTextContent, - }; - - const buildQueryString = (params: LinkRequestQuery) => { - return Object.keys(params) - .filter((key) => params[key as keyof LinkRequestQuery] !== undefined) - .map( - (key) => - `${encodeURIComponent(key)}=${encodeURIComponent( - params[key as keyof LinkRequestQuery] as string - )}` - ) - .join("&"); - }; - - let queryString = buildQueryString(params); - - let basePath; - - if (router.pathname === "/dashboard") basePath = "/api/v1/dashboard"; - else if (router.pathname.startsWith("/public/collections/[id]")) { - queryString = queryString + "&collectionId=" + router.query.id; - basePath = "/api/v1/public/collections/links"; - } else basePath = "/api/v1/links"; - - setIsLoading(true); - - const response = await fetch(`${basePath}?${queryString}`); - - const data = await response.json(); - - setIsLoading(false); - - if (response.ok) setLinks(data.response, isInitialCall); - }; - - useEffect(() => { - // Save the selected links before resetting the links - // and then restore the selected links after resetting the links - const previouslySelected = selectedLinks; - resetLinks(); - - setSelectedLinks(previouslySelected); - getLinks(true); - }, [ - router, - sort, - searchQueryString, - searchByName, - searchByUrl, - searchByDescription, - searchByTextContent, - searchByTags, - ]); - - useEffect(() => { - if (reachedBottom) getLinks(false, links?.at(-1)?.id); - - setReachedBottom(false); - }, [reachedBottom]); - - return { isLoading }; -} diff --git a/hooks/usePermissions.tsx b/hooks/usePermissions.tsx index 7625b49..c672c30 100644 --- a/hooks/usePermissions.tsx +++ b/hooks/usePermissions.tsx @@ -4,9 +4,9 @@ import { useCollections } from "./store/collections"; import { useUser } from "./store/user"; export default function usePermissions(collectionId: number) { - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const [permissions, setPermissions] = useState(); useEffect(() => { diff --git a/layouts/AuthRedirect.tsx b/layouts/AuthRedirect.tsx index 0d07aee..c3a6a3d 100644 --- a/layouts/AuthRedirect.tsx +++ b/layouts/AuthRedirect.tsx @@ -14,7 +14,7 @@ export default function AuthRedirect({ children }: Props) { const router = useRouter(); const { status } = useSession(); const [shouldRenderChildren, setShouldRenderChildren] = useState(false); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); useInitialData(); diff --git a/lib/api/controllers/dashboard/getDashboardData.ts b/lib/api/controllers/dashboard/getDashboardData.ts index 3b0f751..bb2f9e4 100644 --- a/lib/api/controllers/dashboard/getDashboardData.ts +++ b/lib/api/controllers/dashboard/getDashboardData.ts @@ -5,7 +5,7 @@ export default async function getDashboardData( userId: number, query: LinkRequestQuery ) { - let order: any; + let order: any = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" }; @@ -42,7 +42,7 @@ export default async function getDashboardData( select: { id: true }, }, }, - orderBy: order || { id: "desc" }, + orderBy: order, }); const recentlyAddedLinks = await prisma.link.findMany({ @@ -67,10 +67,18 @@ export default async function getDashboardData( select: { id: true }, }, }, - orderBy: order || { id: "desc" }, + orderBy: order, }); - const links = [...recentlyAddedLinks, ...pinnedLinks].sort( + const combinedLinks = [...recentlyAddedLinks, ...pinnedLinks]; + + const uniqueLinks = Array.from( + combinedLinks + .reduce((map, item) => map.set(item.id, item), new Map()) + .values() + ); + + const links = uniqueLinks.sort( (a, b) => (new Date(b.id) as any) - (new Date(a.id) as any) ); diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index 561c919..a6b1dab 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -5,7 +5,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) { const POSTGRES_IS_ENABLED = process.env.DATABASE_URL?.startsWith("postgresql"); - let order: any; + let order: any = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" }; @@ -146,7 +146,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) { select: { id: true }, }, }, - orderBy: order || { id: "desc" }, + orderBy: order, }); return { response: links, status: 200 }; diff --git a/package.json b/package.json index f766af5..f25c334 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "react-hot-toast": "^2.4.1", "react-i18next": "^14.1.2", "react-image-file-resizer": "^0.4.8", + "react-intersection-observer": "^9.13.0", "react-masonry-css": "^1.0.16", "react-select": "^5.7.4", "react-spinners": "^0.13.8", diff --git a/pages/_app.tsx b/pages/_app.tsx index 45f3657..dc6603a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -14,7 +14,13 @@ import { appWithTranslation } from "next-i18next"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 30, + }, + }, +}); function App({ Component, @@ -105,12 +111,3 @@ function App({ } export default appWithTranslation(App); - -// function GetData({ children }: { children: React.ReactNode }) { -// const status = useInitialData(); -// return typeof window !== "undefined" && status !== "loading" ? ( -// children -// ) : ( -// <> -// ); -// } diff --git a/pages/admin.tsx b/pages/admin.tsx index 72d6f2b..df62ffe 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -21,7 +21,7 @@ type UserModal = { export default function Admin() { const { t } = useTranslation(); - const { data: users } = useUsers(); + const { data: users = [] } = useUsers(); const [searchQuery, setSearchQuery] = useState(""); const [filteredUsers, setFilteredUsers] = useState(); diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index f226d2d..c96f807 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -1,4 +1,3 @@ -import useLinkStore from "@/store/links"; import { CollectionIncludingMembersAndLinkCount, Sort, @@ -8,7 +7,6 @@ import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; import ProfilePhoto from "@/components/ProfilePhoto"; -import useLinks from "@/hooks/useLinks"; import usePermissions from "@/hooks/usePermissions"; import NoLinksFound from "@/components/NoLinksFound"; import useLocalSettingsStore from "@/store/localSettings"; @@ -16,16 +14,15 @@ import getPublicUserData from "@/lib/client/getPublicUserData"; import EditCollectionModal from "@/components/ModalContent/EditCollectionModal"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; import DeleteCollectionModal from "@/components/ModalContent/DeleteCollectionModal"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import { dropdownTriggerer } from "@/lib/client/utils"; import NewCollectionModal from "@/components/ModalContent/NewCollectionModal"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import LinkListOptions from "@/components/LinkListOptions"; import { useCollections } from "@/hooks/store/collections"; import { useUser } from "@/hooks/store/user"; +import { useLinks } from "@/hooks/store/links"; +import Links from "@/components/LinkViews/Links"; export default function Index() { const { t } = useTranslation(); @@ -33,25 +30,29 @@ export default function Index() { const router = useRouter(); - const { links } = useLinkStore(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); + + const { links, data } = useLinks({ + sort: sortBy, + collectionId: Number(router.query.id), + }); const [activeCollection, setActiveCollection] = useState(); const permissions = usePermissions(activeCollection?.id as number); - useLinks({ collectionId: Number(router.query.id), sort: sortBy }); - useEffect(() => { setActiveCollection( collections.find((e) => e.id === Number(router.query.id)) ); }, [router, collections]); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); const [collectionOwner, setCollectionOwner] = useState({ id: null as unknown as number, @@ -97,19 +98,10 @@ export default function Index() { if (editMode) return setEditMode(false); }, [router]); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
- {links.some((e) => e.collectionId === Number(router.query.id)) ? ( - e.collection.id === activeCollection?.id - )} - /> - ) : ( - - )} + + {!data.isLoading && !links[0] && }
{activeCollection && ( <> diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 0bf2c7e..051d02a 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -13,8 +13,10 @@ import { useCollections } from "@/hooks/store/collections"; export default function Collections() { const { t } = useTranslation(); - const { data: collections } = useCollections(); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const { data: collections = [] } = useCollections(); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); const [sortedCollections, setSortedCollections] = useState(collections); const { data } = useSession(); diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index a609c6b..d6f540d 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -1,7 +1,5 @@ -import useLinkStore from "@/store/links"; import MainLayout from "@/layouts/MainLayout"; import { useEffect, useState } from "react"; -import useLinks from "@/hooks/useLinks"; import Link from "next/link"; import useWindowDimensions from "@/hooks/useWindowDimensions"; import React from "react"; @@ -10,28 +8,25 @@ import { MigrationFormat, MigrationRequest, ViewMode } from "@/types/global"; import DashboardItem from "@/components/DashboardItem"; import NewLinkModal from "@/components/ModalContent/NewLinkModal"; import PageHeader from "@/components/PageHeader"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import ViewDropdown from "@/components/ViewDropdown"; import { dropdownTriggerer } from "@/lib/client/utils"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useTags } from "@/hooks/store/tags"; +import { useDashboardData } from "@/hooks/store/dashboardData"; +import Links from "@/components/LinkViews/Links"; export default function Dashboard() { const { t } = useTranslation(); - const { data: collections } = useCollections(); - const { links } = useLinkStore(); - const { data: tags } = useTags(); + const { data: collections = [] } = useCollections(); + const dashboardData = useDashboardData(); + const { data: tags = [] } = useTags(); const [numberOfLinks, setNumberOfLinks] = useState(0); const [showLinks, setShowLinks] = useState(3); - useLinks({ pinnedOnly: true, sort: 0 }); - useEffect(() => { setNumberOfLinks( collections.reduce( @@ -81,7 +76,7 @@ export default function Dashboard() { body: JSON.stringify(body), }); - const data = await response.json(); + await response.json(); toast.dismiss(load); @@ -99,20 +94,10 @@ export default function Dashboard() { const [newLinkModal, setNewLinkModal] = useState(false); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - // [ViewMode.Grid]: , - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -171,12 +156,30 @@ export default function Dashboard() {
- {links[0] ? ( + {dashboardData.isLoading ? (
- + +
+ ) : dashboardData.data && + dashboardData.data[0] && + !dashboardData.isLoading ? ( +
+
) : (
@@ -300,12 +303,21 @@ export default function Dashboard() { style={{ flex: "1 1 auto" }} className="flex flex-col 2xl:flex-row items-start 2xl:gap-2" > - {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( + {dashboardData.isLoading ? (
- +
+ ) : dashboardData.data?.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( +
+ e.pinnedBy && e.pinnedBy[0]) .slice(0, showLinks)} + layout={viewMode} />
) : ( diff --git a/pages/links/index.tsx b/pages/links/index.tsx index 1580324..7b1e539 100644 --- a/pages/links/index.tsx +++ b/pages/links/index.tsx @@ -1,29 +1,28 @@ import NoLinksFound from "@/components/NoLinksFound"; -import useLinks from "@/hooks/useLinks"; -// import { useLinks } from "@/hooks/store/links"; +import { useLinks } from "@/hooks/store/links"; import MainLayout from "@/layouts/MainLayout"; -import useLinkStore from "@/store/links"; import React, { useEffect, useState } from "react"; import PageHeader from "@/components/PageHeader"; import { Sort, ViewMode } from "@/types/global"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import { useRouter } from "next/router"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import LinkListOptions from "@/components/LinkListOptions"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; +import Links from "@/components/LinkViews/Links"; -export default function Links() { +export default function Index() { const { t } = useTranslation(); - // const { data: links } = useLinks(); - const { links } = useLinkStore(); - - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); + + const { links, data } = useLinks({ + sort: sortBy, + }); const router = useRouter(); @@ -33,17 +32,6 @@ export default function Links() { if (editMode) return setEditMode(false); }, [router]); - useLinks({ sort: sortBy }); - - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -63,9 +51,14 @@ export default function Links() { /> - {links[0] ? ( - - ) : ( + + {!data.isLoading && !links[0] && ( )}
diff --git a/pages/links/pinned.tsx b/pages/links/pinned.tsx index 60d75d8..3f59c44 100644 --- a/pages/links/pinned.tsx +++ b/pages/links/pinned.tsx @@ -1,45 +1,32 @@ -import useLinks from "@/hooks/useLinks"; import MainLayout from "@/layouts/MainLayout"; -import useLinkStore from "@/store/links"; import React, { useEffect, useState } from "react"; import PageHeader from "@/components/PageHeader"; import { Sort, ViewMode } from "@/types/global"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import { useRouter } from "next/router"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; +import { useLinks } from "@/hooks/store/links"; +import Links from "@/components/LinkViews/Links"; export default function PinnedLinks() { const { t } = useTranslation(); - const { links } = useLinkStore(); - - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card + ); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst ); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); - useLinks({ sort: sortBy, pinnedOnly: true }); + const { links, data } = useLinks({ + sort: sortBy, + pinnedOnly: true, + }); const router = useRouter(); const [editMode, setEditMode] = useState(false); - useEffect(() => { - if (editMode) return setEditMode(false); - }, [router]); - - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -59,9 +46,14 @@ export default function PinnedLinks() { /> - {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( - - ) : ( + + {!data.isLoading && !links[0] && (
(); @@ -18,7 +20,7 @@ export default function Index() { useEffect(() => { const fetchLink = async () => { if (router.query.id) { - await getLink(Number(router.query.id)); + await getLink.mutateAsync(Number(router.query.id)); } }; @@ -26,7 +28,8 @@ export default function Index() { }, []); useEffect(() => { - if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id))); + if (links && links[0]) + setLink(links.find((e) => e.id === Number(router.query.id))); }, [links]); return ( diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 632b831..2700796 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -8,8 +8,6 @@ import { import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import Head from "next/head"; -import useLinks from "@/hooks/useLinks"; -import useLinkStore from "@/store/links"; import ProfilePhoto from "@/components/ProfilePhoto"; import ToggleDarkMode from "@/components/ToggleDarkMode"; import getPublicUserData from "@/lib/client/getPublicUserData"; @@ -18,21 +16,19 @@ import Link from "next/link"; import useLocalSettingsStore from "@/store/localSettings"; import SearchBar from "@/components/SearchBar"; import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; import { useCollections } from "@/hooks/store/collections"; +import { usePublicLinks } from "@/hooks/store/publicLinks"; +import Links from "@/components/LinkViews/Links"; export default function PublicCollections() { const { t } = useTranslation(); - const { links } = useLinkStore(); const { settings } = useLocalSettingsStore(); - const { data: collections } = useCollections(); + const { data: collections = [] } = useCollections(); const router = useRouter(); @@ -54,9 +50,11 @@ export default function PublicCollections() { textContent: false, }); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); - useLinks({ + const { links, data } = usePublicLinks({ sort: sortBy, searchQueryString: router.query.q ? decodeURIComponent(router.query.q as string) @@ -91,19 +89,10 @@ export default function PublicCollections() { const [editCollectionSharingModal, setEditCollectionSharingModal] = useState(false); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return collection ? (
- {links[0] ? ( - e.collectionId === Number(router.query.id)) - .map((e, i) => { - const linkWithCollectionData = { - ...e, - collection: collection, // Append collection data - }; - return linkWithCollectionData; - })} - /> - ) : ( -

{t("collection_is_empty")}

- )} + { + const linkWithCollectionData = { + ...e, + collection: collection, // Append collection data + }; + return linkWithCollectionData; + }) as any + } + layout={viewMode} + placeholderCount={1} + useData={data} + /> + {!data.isLoading && !links[0] &&

{t("collection_is_empty")}

} {/*

List created with Linkwarden. diff --git a/pages/public/preserved/[id].tsx b/pages/public/preserved/[id].tsx index fdb332a..2f415ae 100644 --- a/pages/public/preserved/[id].tsx +++ b/pages/public/preserved/[id].tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import useLinkStore from "@/store/links"; import { useRouter } from "next/router"; import { ArchivedFormat, @@ -7,20 +6,20 @@ import { } from "@/types/global"; import ReadableView from "@/components/ReadableView"; import getServerSideProps from "@/lib/client/getServerSideProps"; +import { useGetLink, useLinks } from "@/hooks/store/links"; export default function Index() { - const { links, getLink } = useLinkStore(); + const { links } = useLinks(); + const getLink = useGetLink(); const [link, setLink] = useState(); const router = useRouter(); - let isPublic = router.pathname.startsWith("/public") ? true : false; - useEffect(() => { const fetchLink = async () => { if (router.query.id) { - await getLink(Number(router.query.id), isPublic); + await getLink.mutateAsync(Number(router.query.id)); } }; @@ -28,7 +27,8 @@ export default function Index() { }, []); useEffect(() => { - if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id))); + if (links && links[0]) + setLink(links.find((e) => e.id === Number(router.query.id))); }, [links]); return ( diff --git a/pages/search.tsx b/pages/search.tsx index d075cd8..e628268 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,23 +1,17 @@ -import useLinks from "@/hooks/useLinks"; +import { useLinks } from "@/hooks/store/links"; import MainLayout from "@/layouts/MainLayout"; -import useLinkStore from "@/store/links"; import { Sort, ViewMode } from "@/types/global"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; import PageHeader from "@/components/PageHeader"; -import { GridLoader } from "react-spinners"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import LinkListOptions from "@/components/LinkListOptions"; import getServerSideProps from "@/lib/client/getServerSideProps"; import { useTranslation } from "next-i18next"; +import Links from "@/components/LinkViews/Links"; export default function Search() { const { t } = useTranslation(); - const { links } = useLinkStore(); - const router = useRouter(); const [searchFilter, setSearchFilter] = useState({ @@ -28,11 +22,13 @@ export default function Search() { textContent: false, }); - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); const [editMode, setEditMode] = useState(false); @@ -40,7 +36,17 @@ export default function Search() { if (editMode) return setEditMode(false); }, [router]); - const { isLoading } = useLinks({ + // const { isLoading } = useLink({ + // sort: sortBy, + // searchQueryString: decodeURIComponent(router.query.q as string), + // searchByName: searchFilter.name, + // searchByUrl: searchFilter.url, + // searchByDescription: searchFilter.description, + // searchByTextContent: searchFilter.textContent, + // searchByTags: searchFilter.tags, + // }); + + const { links, data } = useLinks({ sort: sortBy, searchQueryString: decodeURIComponent(router.query.q as string), searchByName: searchFilter.name, @@ -50,15 +56,6 @@ export default function Search() { searchByTags: searchFilter.tags, }); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (

@@ -76,7 +73,9 @@ export default function Search() { - {!isLoading && !links[0] ? ( + {/* { + !isLoading && + !links[0] ? (

{t("nothing_found")}

) : links[0] ? ( ) - )} + )} */} + +
); diff --git a/pages/settings/access-tokens.tsx b/pages/settings/access-tokens.tsx index 91d1761..9e1cfd6 100644 --- a/pages/settings/access-tokens.tsx +++ b/pages/settings/access-tokens.tsx @@ -18,7 +18,7 @@ export default function AccessTokens() { setRevokeTokenModal(true); }; - const { data: tokens } = useTokens(); + const { data: tokens = [] } = useTokens(); return ( diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx index fe8bfd2..555798c 100644 --- a/pages/subscribe.tsx +++ b/pages/subscribe.tsx @@ -20,7 +20,7 @@ export default function Subscribe() { const router = useRouter(); - const { data: user } = useUser(); + const { data: user = {} } = useUser(); useEffect(() => { const hasInactiveSubscription = diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 92fffe7..e71fe84 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -1,31 +1,28 @@ -import useLinkStore from "@/store/links"; import { useRouter } from "next/router"; import { FormEvent, useEffect, useState } from "react"; import MainLayout from "@/layouts/MainLayout"; import { Sort, TagIncludingLinkCount, ViewMode } from "@/types/global"; -import useLinks from "@/hooks/useLinks"; -import { toast } from "react-hot-toast"; -import CardView from "@/components/LinkViews/Layouts/CardView"; -import ListView from "@/components/LinkViews/Layouts/ListView"; +import { useLinks } from "@/hooks/store/links"; import { dropdownTriggerer } from "@/lib/client/utils"; import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal"; import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal"; -import MasonryView from "@/components/LinkViews/Layouts/MasonryView"; import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; import { useRemoveTag, useTags, useUpdateTag } from "@/hooks/store/tags"; +import Links from "@/components/LinkViews/Links"; export default function Index() { const { t } = useTranslation(); const router = useRouter(); - const { links } = useLinkStore(); - const { data: tags } = useTags(); + const { data: tags = [] } = useTags(); const updateTag = useUpdateTag(); const removeTag = useRemoveTag(); - const [sortBy, setSortBy] = useState(Sort.DateNewestFirst); + const [sortBy, setSortBy] = useState( + Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst + ); const [renameTag, setRenameTag] = useState(false); const [newTagName, setNewTagName] = useState(); @@ -40,7 +37,10 @@ export default function Index() { if (editMode) return setEditMode(false); }, [router]); - useLinks({ tagId: Number(router.query.id), sort: sortBy }); + const { links, data } = useLinks({ + sort: sortBy, + tagId: Number(router.query.id), + }); useEffect(() => { const tag = tags.find((e: any) => e.id === Number(router.query.id)); @@ -98,19 +98,10 @@ export default function Index() { setRenameTag(false); }; - const [viewMode, setViewMode] = useState( - localStorage.getItem("viewMode") || ViewMode.Card + const [viewMode, setViewMode] = useState( + (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card ); - const linkView = { - [ViewMode.Card]: CardView, - [ViewMode.List]: ListView, - [ViewMode.Masonry]: MasonryView, - }; - - // @ts-ignore - const LinkComponent = linkView[viewMode]; - return (
@@ -210,11 +201,12 @@ export default function Index() {
- - e.tags.some((e) => e.id === Number(router.query.id)) - )} + links={links} + layout={viewMode} + placeholderCount={1} + useData={data} />
{bulkDeleteLinksModal && ( diff --git a/store/links.ts b/store/links.ts index 9339b6e..18fbe15 100644 --- a/store/links.ts +++ b/store/links.ts @@ -1,8 +1,5 @@ import { create } from "zustand"; -import { - ArchivedFormat, - LinkIncludingShortenedCollectionAndTags, -} from "@/types/global"; +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; type ResponseObject = { ok: boolean; @@ -10,24 +7,8 @@ type ResponseObject = { }; type LinkStore = { - links: LinkIncludingShortenedCollectionAndTags[]; selectedLinks: LinkIncludingShortenedCollectionAndTags[]; - setLinks: ( - data: LinkIncludingShortenedCollectionAndTags[], - isInitialCall: boolean - ) => void; setSelectedLinks: (links: LinkIncludingShortenedCollectionAndTags[]) => void; - addLink: ( - body: LinkIncludingShortenedCollectionAndTags - ) => Promise; - uploadFile: ( - link: LinkIncludingShortenedCollectionAndTags, - file: File - ) => Promise; - getLink: (linkId: number, publicRoute?: boolean) => Promise; - updateLink: ( - link: LinkIncludingShortenedCollectionAndTags - ) => Promise; updateLinks: ( links: LinkIncludingShortenedCollectionAndTags[], removePreviousTags: boolean, @@ -36,180 +17,11 @@ type LinkStore = { "tags" | "collectionId" > ) => Promise; - removeLink: (linkId: number) => Promise; - deleteLinksById: (linkIds: number[]) => Promise; - resetLinks: () => void; }; const useLinkStore = create()((set) => ({ - links: [], selectedLinks: [], - setLinks: async (data, isInitialCall) => { - isInitialCall && - set(() => ({ - links: [], - })); - set((state) => ({ - // Filter duplicate links by id - links: [...state.links, ...data].reduce( - (links: LinkIncludingShortenedCollectionAndTags[], item) => { - if (!links.some((link) => link.id === item.id)) { - links.push(item); - } - return links; - }, - [] - ), - })); - }, setSelectedLinks: (links) => set({ selectedLinks: links }), - addLink: async (body) => { - const response = await fetch("/api/v1/links", { - body: JSON.stringify(body), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: [data.response, ...state.links], - })); - } - - return { ok: response.ok, data: data.response }; - }, - uploadFile: async (link, file) => { - let fileType: ArchivedFormat | null = null; - let linkType: "url" | "image" | "pdf" | null = null; - - if (file?.type === "image/jpg" || file.type === "image/jpeg") { - fileType = ArchivedFormat.jpeg; - linkType = "image"; - } else if (file.type === "image/png") { - fileType = ArchivedFormat.png; - linkType = "image"; - } else if (file.type === "application/pdf") { - fileType = ArchivedFormat.pdf; - linkType = "pdf"; - } else { - return { ok: false, data: "Invalid file type." }; - } - - const response = await fetch("/api/v1/links", { - body: JSON.stringify({ - ...link, - type: linkType, - name: link.name ? link.name : file.name, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }); - - const data = await response.json(); - - const createdLink: LinkIncludingShortenedCollectionAndTags = data.response; - - console.log(data); - - if (response.ok) { - const formBody = new FormData(); - file && formBody.append("file", file); - - await fetch( - `/api/v1/archives/${(data as any).response.id}?format=${fileType}`, - { - body: formBody, - method: "POST", - } - ); - - // get file extension - const extension = file.name.split(".").pop() || ""; - - set((state) => ({ - links: [ - { - ...createdLink, - image: - linkType === "image" - ? `archives/${createdLink.collectionId}/${ - createdLink.id + extension - }` - : null, - pdf: - linkType === "pdf" - ? `archives/${createdLink.collectionId}/${ - createdLink.id + ".pdf" - }` - : null, - }, - ...state.links, - ], - })); - } - - return { ok: response.ok, data: data.response }; - }, - getLink: async (linkId, publicRoute) => { - const path = publicRoute - ? `/api/v1/public/links/${linkId}` - : `/api/v1/links/${linkId}`; - - const response = await fetch(path); - - const data = await response.json(); - - if (response.ok) { - set((state) => { - const linkExists = state.links.some( - (link) => link.id === data.response.id - ); - - if (linkExists) { - return { - links: state.links.map((e) => - e.id === data.response.id ? data.response : e - ), - }; - } else { - return { - links: [...state.links, data.response], - }; - } - }); - - return data; - } - - return { ok: response.ok, data: data.response }; - }, - updateLink: async (link) => { - const response = await fetch(`/api/v1/links/${link.id}`, { - body: JSON.stringify(link), - headers: { - "Content-Type": "application/json", - }, - method: "PUT", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: state.links.map((e) => - e.id === data.response.id ? data.response : e - ), - })); - } - - return { ok: response.ok, data: data.response }; - }, updateLinks: async (links, removePreviousTags, newData) => { const response = await fetch("/api/v1/links", { body: JSON.stringify({ links, removePreviousTags, newData }), @@ -222,65 +34,11 @@ const useLinkStore = create()((set) => ({ const data = await response.json(); if (response.ok) { - set((state) => ({ - links: state.links.map((e) => - links.some((link) => link.id === e.id) - ? { - ...e, - collectionId: newData.collectionId ?? e.collectionId, - collection: { - ...e.collection, - id: newData.collectionId ?? e.collection.id, - }, - tags: removePreviousTags - ? [...(newData.tags ?? [])] - : [...e.tags, ...(newData.tags ?? [])], - } - : e - ), - })); + // Update the selected links with the new data } return { ok: response.ok, data: data.response }; }, - removeLink: async (linkId) => { - const response = await fetch(`/api/v1/links/${linkId}`, { - headers: { - "Content-Type": "application/json", - }, - method: "DELETE", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: state.links.filter((e) => e.id !== linkId), - })); - } - - return { ok: response.ok, data: data.response }; - }, - deleteLinksById: async (linkIds: number[]) => { - const response = await fetch("/api/v1/links", { - body: JSON.stringify({ linkIds }), - headers: { - "Content-Type": "application/json", - }, - method: "DELETE", - }); - - const data = await response.json(); - - if (response.ok) { - set((state) => ({ - links: state.links.filter((e) => !linkIds.includes(e.id as number)), - })); - } - - return { ok: response.ok, data: data.response }; - }, - resetLinks: () => set({ links: [] }), })); export default useLinkStore; diff --git a/store/localSettings.ts b/store/localSettings.ts index 6c79d6b..864fa28 100644 --- a/store/localSettings.ts +++ b/store/localSettings.ts @@ -1,8 +1,10 @@ +import { Sort } from "@/types/global"; import { create } from "zustand"; type LocalSettings = { theme?: string; viewMode?: string; + sortBy?: Sort; }; type LocalSettingsStore = { @@ -15,10 +17,11 @@ const useLocalSettingsStore = create((set) => ({ settings: { theme: "", viewMode: "", + sortBy: Sort.DateNewestFirst, }, updateSettings: async (newSettings) => { if ( - newSettings.theme && + newSettings.theme !== undefined && newSettings.theme !== localStorage.getItem("theme") ) { localStorage.setItem("theme", newSettings.theme); @@ -29,7 +32,7 @@ const useLocalSettingsStore = create((set) => ({ } if ( - newSettings.viewMode && + newSettings.viewMode !== undefined && newSettings.viewMode !== localStorage.getItem("viewMode") ) { localStorage.setItem("viewMode", newSettings.viewMode); @@ -37,6 +40,13 @@ const useLocalSettingsStore = create((set) => ({ // const localTheme = localStorage.getItem("viewMode") || ""; } + if ( + newSettings.sortBy !== undefined && + newSettings.sortBy !== Number(localStorage.getItem("sortBy")) + ) { + localStorage.setItem("sortBy", newSettings.sortBy.toString()); + } + set((state) => ({ settings: { ...state.settings, ...newSettings } })); }, setSettings: async () => { diff --git a/types/global.ts b/types/global.ts index 90c9aef..1cbbde8 100644 --- a/types/global.ts +++ b/types/global.ts @@ -67,7 +67,6 @@ export interface PublicCollectionIncludingLinks extends Collection { export enum ViewMode { Card = "card", - Grid = "grid", List = "list", Masonry = "masonry", } @@ -82,7 +81,7 @@ export enum Sort { } export type LinkRequestQuery = { - sort: Sort; + sort?: Sort; cursor?: number; collectionId?: number; tagId?: number; diff --git a/yarn.lock b/yarn.lock index 174222e..1aecfae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5274,6 +5274,11 @@ react-image-file-resizer@^0.4.8: resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af" integrity sha512-Ue7CfKnSlsfJ//SKzxNMz8avDgDSpWQDOnTKOp/GNRFJv4dO9L5YGHNEnj40peWkXXAK2OK0eRIoXhOYpUzUTQ== +react-intersection-observer@^9.13.0: + version "9.13.0" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz#ee10827954cf6ccc204d027f8400a6ddb8df163a" + integrity sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" From bd34dacf21dc0d18ea761c8e61d7168c5f540a8f Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 13 Aug 2024 03:01:02 -0400 Subject: [PATCH 19/33] bugs fixed --- components/LinkViews/Layouts/CardView.tsx | 60 ----------------- components/LinkViews/Layouts/ListView.tsx | 38 ----------- components/LinkViews/Layouts/MasonryView.tsx | 58 ---------------- .../LinkViews/LinkComponents/LinkActions.tsx | 2 +- .../{ => LinkComponents}/LinkCard.tsx | 4 +- .../{ => LinkComponents}/LinkList.tsx | 2 +- .../{ => LinkComponents}/LinkMasonry.tsx | 4 +- components/LinkViews/Links.tsx | 37 ++--------- .../ModalContent/BulkEditLinksModal.tsx | 31 +++++---- .../ModalContent/PreservedFormatsModal.tsx | 30 +++------ components/PreserverdFormatRow.tsx | 37 +---------- hooks/store/links.tsx | 66 +++++++++---------- package.json | 2 +- yarn.lock | 8 +-- 14 files changed, 73 insertions(+), 306 deletions(-) delete mode 100644 components/LinkViews/Layouts/CardView.tsx delete mode 100644 components/LinkViews/Layouts/ListView.tsx delete mode 100644 components/LinkViews/Layouts/MasonryView.tsx rename components/LinkViews/{ => LinkComponents}/LinkCard.tsx (98%) rename components/LinkViews/{ => LinkComponents}/LinkList.tsx (98%) rename components/LinkViews/{ => LinkComponents}/LinkMasonry.tsx (98%) diff --git a/components/LinkViews/Layouts/CardView.tsx b/components/LinkViews/Layouts/CardView.tsx deleted file mode 100644 index 8e85431..0000000 --- a/components/LinkViews/Layouts/CardView.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import LinkCard from "@/components/LinkViews/LinkCard"; -import { useLinks } from "@/hooks/store/links"; -import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; -import { useEffect } from "react"; -import { useInView } from "react-intersection-observer"; -import { GridLoader } from "react-spinners"; - -export default function CardView({ - links, - editMode, - isLoading, -}: { - links: LinkIncludingShortenedCollectionAndTags[]; - editMode?: boolean; - isLoading?: boolean; -}) { - const { ref, inView } = useInView(); - - const { data } = useLinks(); - - useEffect(() => { - if (inView) { - data.fetchNextPage(); - } - }, [data.fetchNextPage, inView]); - - return ( -
- {links.map((e, i) => { - return ( - - ); - })} - - {data.hasNextPage && ( -
-
-
-
-
-
- )} - - {isLoading && links.length > 0 && ( - - )} -
- ); -} diff --git a/components/LinkViews/Layouts/ListView.tsx b/components/LinkViews/Layouts/ListView.tsx deleted file mode 100644 index a83609c..0000000 --- a/components/LinkViews/Layouts/ListView.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import LinkList from "@/components/LinkViews/LinkList"; -import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; -import { GridLoader } from "react-spinners"; - -export default function ListView({ - links, - editMode, - isLoading, -}: { - links: LinkIncludingShortenedCollectionAndTags[]; - editMode?: boolean; - isLoading?: boolean; -}) { - return ( -
- {links.map((e, i) => { - return ( - - ); - })} - - {isLoading && links.length > 0 && ( - - )} -
- ); -} diff --git a/components/LinkViews/Layouts/MasonryView.tsx b/components/LinkViews/Layouts/MasonryView.tsx deleted file mode 100644 index bceaf33..0000000 --- a/components/LinkViews/Layouts/MasonryView.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import LinkMasonry from "@/components/LinkViews/LinkMasonry"; -import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; -import { GridLoader } from "react-spinners"; -import Masonry from "react-masonry-css"; -import resolveConfig from "tailwindcss/resolveConfig"; -import tailwindConfig from "../../../tailwind.config.js"; -import { useMemo } from "react"; - -export default function MasonryView({ - links, - editMode, - isLoading, -}: { - links: LinkIncludingShortenedCollectionAndTags[]; - editMode?: boolean; - isLoading?: boolean; -}) { - const fullConfig = resolveConfig(tailwindConfig as any); - - const breakpointColumnsObj = useMemo(() => { - return { - default: 5, - 1900: 4, - 1500: 3, - 880: 2, - 550: 1, - }; - }, []); - - return ( - - {links.map((e, i) => { - return ( - - ); - })} - - {isLoading && links.length > 0 && ( - - )} - - ); -} diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index ffa0051..6d55d85 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -162,7 +162,7 @@ export default function LinkActions({ {preservedFormatsModal ? ( setPreservedFormatsModal(false)} - activeLink={link} + link={link} /> ) : undefined} {/* {expandedLink ? ( diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkComponents/LinkCard.tsx similarity index 98% rename from components/LinkViews/LinkCard.tsx rename to components/LinkViews/LinkComponents/LinkCard.tsx index b6e764b..e228fb1 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkComponents/LinkCard.tsx @@ -12,12 +12,12 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection import Image from "next/image"; import { previewAvailable } from "@/lib/shared/getArchiveValidity"; import Link from "next/link"; -import LinkIcon from "./LinkComponents/LinkIcon"; +import LinkIcon from "./LinkIcon"; import useOnScreen from "@/hooks/useOnScreen"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; -import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; +import LinkTypeBadge from "./LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useUser } from "@/hooks/store/user"; diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkComponents/LinkList.tsx similarity index 98% rename from components/LinkViews/LinkList.tsx rename to components/LinkViews/LinkComponents/LinkList.tsx index ac17c68..d3c8d40 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkComponents/LinkList.tsx @@ -13,7 +13,7 @@ import { isPWA } from "@/lib/client/utils"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; -import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; +import LinkTypeBadge from "./LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useUser } from "@/hooks/store/user"; diff --git a/components/LinkViews/LinkMasonry.tsx b/components/LinkViews/LinkComponents/LinkMasonry.tsx similarity index 98% rename from components/LinkViews/LinkMasonry.tsx rename to components/LinkViews/LinkComponents/LinkMasonry.tsx index 0c20395..3d327a1 100644 --- a/components/LinkViews/LinkMasonry.tsx +++ b/components/LinkViews/LinkComponents/LinkMasonry.tsx @@ -12,12 +12,12 @@ import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection import Image from "next/image"; import { previewAvailable } from "@/lib/shared/getArchiveValidity"; import Link from "next/link"; -import LinkIcon from "./LinkComponents/LinkIcon"; +import LinkIcon from "./LinkIcon"; import useOnScreen from "@/hooks/useOnScreen"; import { generateLinkHref } from "@/lib/client/generateLinkHref"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; -import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; +import LinkTypeBadge from "./LinkTypeBadge"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useUser } from "@/hooks/store/user"; diff --git a/components/LinkViews/Links.tsx b/components/LinkViews/Links.tsx index a90cf77..11720dc 100644 --- a/components/LinkViews/Links.tsx +++ b/components/LinkViews/Links.tsx @@ -1,18 +1,16 @@ -import LinkCard from "@/components/LinkViews/LinkCard"; -import { useLinks } from "@/hooks/store/links"; +import LinkCard from "@/components/LinkViews/LinkComponents/LinkCard"; import { LinkIncludingShortenedCollectionAndTags, ViewMode, } from "@/types/global"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useInView } from "react-intersection-observer"; -import { GridLoader } from "react-spinners"; -import LinkMasonry from "@/components/LinkViews/LinkMasonry"; +import LinkMasonry from "@/components/LinkViews/LinkComponents/LinkMasonry"; import Masonry from "react-masonry-css"; import resolveConfig from "tailwindcss/resolveConfig"; import tailwindConfig from "../../tailwind.config.js"; import { useMemo } from "react"; -import LinkList from "@/components/LinkViews/LinkList"; +import LinkList from "@/components/LinkViews/LinkComponents/LinkList"; export function CardView({ links, @@ -59,15 +57,6 @@ export function CardView({
); })} - - {/* {isLoading && links.length > 0 && ( - - )} */}
); } @@ -100,15 +89,6 @@ export function ListView({ /> ); })} - - {/* {isLoading && links.length > 0 && ( - - )} */}
); } @@ -157,15 +137,6 @@ export function MasonryView({ /> ); })} - - {/* {isLoading && links.length > 0 && ( - - )} */} ); } diff --git a/components/ModalContent/BulkEditLinksModal.tsx b/components/ModalContent/BulkEditLinksModal.tsx index 5f3d31a..3797e38 100644 --- a/components/ModalContent/BulkEditLinksModal.tsx +++ b/components/ModalContent/BulkEditLinksModal.tsx @@ -6,6 +6,7 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import toast from "react-hot-toast"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; +import { useBulkEditLinks } from "@/hooks/store/links"; type Props = { onClose: Function; @@ -13,13 +14,14 @@ type Props = { export default function BulkEditLinksModal({ onClose }: Props) { const { t } = useTranslation(); - const { updateLinks, selectedLinks, setSelectedLinks } = useLinkStore(); + const { selectedLinks, setSelectedLinks } = useLinkStore(); const [submitLoader, setSubmitLoader] = useState(false); const [removePreviousTags, setRemovePreviousTags] = useState(false); const [updatedValues, setUpdatedValues] = useState< Pick >({ tags: [] }); + const updateLinks = useBulkEditLinks(); const setCollection = (e: any) => { const collectionId = e?.value || null; setUpdatedValues((prevValues) => ({ ...prevValues, collectionId })); @@ -34,24 +36,21 @@ export default function BulkEditLinksModal({ onClose }: Props) { if (!submitLoader) { setSubmitLoader(true); - const load = toast.loading(t("updating")); - - const response = await updateLinks( - selectedLinks, - removePreviousTags, - updatedValues + await updateLinks.mutateAsync( + { + links: selectedLinks, + newData: updatedValues, + removePreviousTags, + }, + { + onSuccess: () => { + setSelectedLinks([]); + onClose(); + }, + } ); - toast.dismiss(load); - - if (response.ok) { - toast.success(t("updated")); - setSelectedLinks([]); - onClose(); - } else toast.error(response.data as string); - setSubmitLoader(false); - return response; } }; diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 3d2e29d..7cd1df0 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -23,16 +23,14 @@ import { useGetLink } from "@/hooks/store/links"; type Props = { onClose: Function; - activeLink: LinkIncludingShortenedCollectionAndTags; + link: LinkIncludingShortenedCollectionAndTags; }; -export default function PreservedFormatsModal({ onClose, activeLink }: Props) { +export default function PreservedFormatsModal({ onClose, link }: Props) { const { t } = useTranslation(); const session = useSession(); const getLink = useGetLink(); const { data: user = {} } = useUser(); - const [link, setLink] = - useState(activeLink); const router = useRouter(); let isPublic = router.pathname.startsWith("/public") ? true : undefined; @@ -98,20 +96,14 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { useEffect(() => { (async () => { - const data = await getLink.mutateAsync(link.id as number); - setLink( - (data as any).response as LinkIncludingShortenedCollectionAndTags - ); + await getLink.mutateAsync(link.id as number); })(); let interval: any; if (!isReady()) { interval = setInterval(async () => { - const data = await getLink.mutateAsync(link.id as number); - setLink( - (data as any).response as LinkIncludingShortenedCollectionAndTags - ); + await getLink.mutateAsync(link.id as number); }, 5000); } else { if (interval) { @@ -137,10 +129,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { toast.dismiss(load); if (response.ok) { - const newLink = await getLink.mutateAsync(link?.id as number); - setLink( - (newLink as any).response as LinkIncludingShortenedCollectionAndTags - ); + await getLink.mutateAsync(link?.id as number); + toast.success(t("link_being_archived")); } else toast.error(data.response); }; @@ -164,7 +154,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { name={t("webpage")} icon={"bi-filetype-html"} format={ArchivedFormat.monolith} - activeLink={link} + link={link} downloadable={true} /> ) : undefined} @@ -178,7 +168,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { ? ArchivedFormat.png : ArchivedFormat.jpeg } - activeLink={link} + link={link} downloadable={true} /> ) : undefined} @@ -188,7 +178,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { name={t("pdf")} icon={"bi-file-earmark-pdf"} format={ArchivedFormat.pdf} - activeLink={link} + link={link} downloadable={true} /> ) : undefined} @@ -198,7 +188,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) { name={t("readable")} icon={"bi-file-earmark-text"} format={ArchivedFormat.readability} - activeLink={link} + link={link} /> ) : undefined} diff --git a/components/PreserverdFormatRow.tsx b/components/PreserverdFormatRow.tsx index 2e18e39..e8ddbd1 100644 --- a/components/PreserverdFormatRow.tsx +++ b/components/PreserverdFormatRow.tsx @@ -1,4 +1,3 @@ -import React, { useEffect, useState } from "react"; import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags, @@ -11,7 +10,7 @@ type Props = { name: string; icon: string; format: ArchivedFormat; - activeLink: LinkIncludingShortenedCollectionAndTags; + link: LinkIncludingShortenedCollectionAndTags; downloadable?: boolean; }; @@ -19,47 +18,15 @@ export default function PreservedFormatRow({ name, icon, format, - activeLink, + link, downloadable, }: Props) { const getLink = useGetLink(); - const [link, setLink] = - useState(activeLink); - const router = useRouter(); let isPublic = router.pathname.startsWith("/public") ? true : undefined; - useEffect(() => { - (async () => { - const data = await getLink.mutateAsync(link.id as number); - setLink( - (data as any).response as LinkIncludingShortenedCollectionAndTags - ); - })(); - - let interval: any; - if (link?.image === "pending" || link?.pdf === "pending") { - interval = setInterval(async () => { - const data = await getLink.mutateAsync(link.id as number); - setLink( - (data as any).response as LinkIncludingShortenedCollectionAndTags - ); - }, 5000); - } else { - if (interval) { - clearInterval(interval); - } - } - - return () => { - if (interval) { - clearInterval(interval); - } - }; - }, [link?.image, link?.pdf, link?.readable, link?.monolith]); - const handleDownload = () => { const path = `/api/v1/archives/${link?.id}?format=${format}`; fetch(path) diff --git a/hooks/store/links.tsx b/hooks/store/links.tsx index 93b9b75..2fd7c2b 100644 --- a/hooks/store/links.tsx +++ b/hooks/store/links.tsx @@ -129,7 +129,7 @@ const useAddLink = () => { return [data, ...oldData]; }); - queryClient.setQueryData(["links"], (oldData: any) => { + queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { if (!oldData) return undefined; return { pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)], @@ -179,13 +179,12 @@ const useUpdateLink = () => { return oldData.map((e: any) => (e.id === data.id ? data : e)); }); - queryClient.setQueryData(["links"], (oldData: any) => { + queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { if (!oldData) return undefined; return { - pages: [ - oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)), - ...oldData.pages.slice(1), - ], + pages: oldData.pages.map((page: any) => + page.map((item: any) => (item.id === data.id ? data : item)) + ), pageParams: oldData.pageParams, }; }); @@ -228,13 +227,12 @@ const useDeleteLink = () => { return oldData.filter((e: any) => e.id !== data.id); }); - queryClient.setQueryData(["links"], (oldData: any) => { + queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { if (!oldData) return undefined; return { - pages: [ - oldData.pages[0].filter((e: any) => e.id !== data.id), - ...oldData.pages.slice(1), - ], + pages: oldData.pages.map((page: any) => + page.filter((item: any) => item.id !== data.id) + ), pageParams: oldData.pageParams, }; }); @@ -267,13 +265,12 @@ const useGetLink = () => { return oldData.map((e: any) => (e.id === data.id ? data : e)); }); - queryClient.setQueryData(["links"], (oldData: any) => { + queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { if (!oldData) return undefined; return { - pages: [ - oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)), - ...oldData.pages.slice(1), - ], + pages: oldData.pages.map((page: any) => + page.map((item: any) => (item.id === data.id ? data : item)) + ), pageParams: oldData.pageParams, }; }); @@ -315,13 +312,12 @@ const useBulkDeleteLinks = () => { return oldData.filter((e: any) => !data.includes(e.id)); }); - queryClient.setQueryData(["links"], (oldData: any) => { + queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { if (!oldData) return undefined; return { - pages: [ - oldData.pages[0].filter((e: any) => !data.includes(e.id)), - ...oldData.pages.slice(1), - ], + pages: oldData.pages.map((page: any) => + page.filter((item: any) => !data.includes(item.id)) + ), pageParams: oldData.pageParams, }; }); @@ -401,7 +397,7 @@ const useUploadFile = () => { return [data, ...oldData]; }); - queryClient.setQueryData(["links"], (oldData: any) => { + queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { if (!oldData) return undefined; return { pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)], @@ -454,7 +450,7 @@ const useBulkEditLinks = () => { return data.response; }, - onSuccess: (data) => { + onSuccess: (data, { links, newData, removePreviousTags }) => { toast.success(t("updated")); queryClient.setQueryData(["dashboardData"], (oldData: any) => { @@ -464,18 +460,18 @@ const useBulkEditLinks = () => { ); }); - queryClient.setQueryData(["links"], (oldData: any) => { - if (!oldData) return undefined; - return { - pages: [ - oldData.pages[0].map((e: any) => - data.find((d: any) => d.id === e.id) ? data : e - ), - ...oldData.pages.slice(1), - ], - pageParams: oldData.pageParams, - }; - }); + // TODO: Fix this + // queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { + // if (!oldData) return undefined; + // return { + // pages: oldData.pages.map((page: any) => for (item of links) { + // page.map((item: any) => (item.id === data.id ? data : item)) + // } + // ), + // pageParams: oldData.pageParams, + // }; + // }); + queryClient.invalidateQueries({ queryKey: ["links"] }); // Temporary workaround queryClient.invalidateQueries({ queryKey: ["collections"] }); queryClient.invalidateQueries({ queryKey: ["tags"] }); diff --git a/package.json b/package.json index f25c334..f2a357e 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "react-intersection-observer": "^9.13.0", "react-masonry-css": "^1.0.16", "react-select": "^5.7.4", - "react-spinners": "^0.13.8", + "react-spinners": "^0.14.1", "socks-proxy-agent": "^8.0.2", "stripe": "^12.13.0", "tailwind-merge": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index 1aecfae..edf5040 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5340,10 +5340,10 @@ react-select@^5.7.4: react-transition-group "^4.3.0" use-isomorphic-layout-effect "^1.1.2" -react-spinners@^0.13.8: - version "0.13.8" - resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc" - integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA== +react-spinners@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.14.1.tgz#de7d7d6b3e6d4f29d9620c65495b502c7dd90812" + integrity sha512-2Izq+qgQ08HTofCVEdcAQCXFEYfqTDdfeDQJeo/HHQiQJD4imOicNLhkfN2eh1NYEWVOX4D9ok2lhuDB0z3Aag== react-style-singleton@^2.2.1: version "2.2.1" From bc04ea0fe8e23837d12d81b9701aab7611e66feb Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Tue, 13 Aug 2024 03:19:28 -0400 Subject: [PATCH 20/33] fixed other views alongside card view --- components/LinkViews/Links.tsx | 105 +++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/components/LinkViews/Links.tsx b/components/LinkViews/Links.tsx index 11720dc..511c1d2 100644 --- a/components/LinkViews/Links.tsx +++ b/components/LinkViews/Links.tsx @@ -61,38 +61,6 @@ export function CardView({ ); } -export function ListView({ - links, - editMode, - isLoading, - placeholders, - hasNextPage, - placeHolderRef, -}: { - links?: LinkIncludingShortenedCollectionAndTags[]; - editMode?: boolean; - isLoading?: boolean; - placeholders?: number[]; - hasNextPage?: boolean; - placeHolderRef?: any; -}) { - return ( -
- {links?.map((e, i) => { - return ( - - ); - })} -
- ); -} - export function MasonryView({ links, editMode, @@ -137,10 +105,77 @@ export function MasonryView({ /> ); })} + + {(hasNextPage || isLoading) && + placeholders?.map((e, i) => { + return ( +
+
+
+
+
+
+
+ ); + })} ); } +export function ListView({ + links, + editMode, + isLoading, + placeholders, + hasNextPage, + placeHolderRef, +}: { + links?: LinkIncludingShortenedCollectionAndTags[]; + editMode?: boolean; + isLoading?: boolean; + placeholders?: number[]; + hasNextPage?: boolean; + placeHolderRef?: any; +}) { + return ( +
+ {links?.map((e, i) => { + return ( + + ); + })} + + {(hasNextPage || isLoading) && + placeholders?.map((e, i) => { + return ( +
+
+
+
+
+
+
+
+ ); + })} +
+ ); +} + export default function Links({ layout, links, @@ -168,6 +203,9 @@ export default function Links({ links={links} editMode={editMode} isLoading={useData?.isLoading} + placeholders={placeholderCountToArray(placeholderCount)} + hasNextPage={useData?.hasNextPage} + placeHolderRef={ref} /> ); } else if (layout === ViewMode.Masonry) { @@ -176,6 +214,9 @@ export default function Links({ links={links} editMode={editMode} isLoading={useData?.isLoading} + placeholders={placeholderCountToArray(placeholderCount)} + hasNextPage={useData?.hasNextPage} + placeHolderRef={ref} /> ); } else { From d15d965139ee4b6f7027f2f0b8f8ccfcf02b44ed Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Wed, 14 Aug 2024 13:14:06 -0400 Subject: [PATCH 21/33] added skeleton loading --- components/CollectionListing.tsx | 12 ++++++++++-- components/Sidebar.tsx | 10 ++++++++-- pages/dashboard.tsx | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index f118184..8abc080 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -25,7 +25,7 @@ interface ExtendedTreeItem extends TreeItem { const CollectionListing = () => { const { t } = useTranslation(); const updateCollection = useUpdateCollection(); - const { data: collections = [] } = useCollections(); + const { data: collections = [], isLoading } = useCollections(); const { data: user = {} } = useUser(); const updateUser = useUpdateUser(); @@ -204,7 +204,15 @@ const CollectionListing = () => { } }; - if (!tree) { + if (isLoading) { + return ( +
+
+
+
+
+ ); + } else if (!tree) { return (

{t("you_have_no_collections")} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index dc15c61..645fda4 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -24,7 +24,7 @@ export default function Sidebar({ className }: { className?: string }) { const { data: collections } = useCollections(); - const { data: tags = [] } = useTags(); + const { data: tags = [], isLoading } = useTags(); const [active, setActive] = useState(""); const router = useRouter(); @@ -128,7 +128,13 @@ export default function Sidebar({ className }: { className?: string }) { leaveTo="transform opacity-0 -translate-y-3" > - {tags[0] ? ( + {isLoading ? ( +

+
+
+
+
+ ) : tags[0] ? ( tags .sort((a: any, b: any) => a.name.localeCompare(b.name)) .map((e: any, i: any) => { diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index d6f540d..b9ba86d 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -168,7 +168,7 @@ export default function Dashboard() {
@@ -307,7 +307,7 @@ export default function Dashboard() {
From 9cc3a7206e26a368bd751a8590a952c4962cd131 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Wed, 14 Aug 2024 15:22:28 -0400 Subject: [PATCH 22/33] changes and improvements --- components/CollectionListing.tsx | 25 +++++--- components/LinkListOptions.tsx | 14 +++- .../LinkViews/LinkComponents/LinkActions.tsx | 44 +++++++++++-- .../ModalContent/BulkDeleteLinksModal.tsx | 16 ++++- .../ModalContent/BulkEditLinksModal.tsx | 15 ++++- .../ModalContent/DeleteCollectionModal.tsx | 16 ++++- components/ModalContent/DeleteLinkModal.tsx | 21 ++++-- .../ModalContent/EditCollectionModal.tsx | 14 +++- .../EditCollectionSharingModal.tsx | 13 +++- components/ModalContent/EditLinkModal.tsx | 14 +++- .../ModalContent/NewCollectionModal.tsx | 14 +++- components/ModalContent/NewLinkModal.tsx | 14 +++- components/ModalContent/NewTokenModal.tsx | 12 +++- components/ModalContent/RevokeTokenModal.tsx | 14 +++- components/ModalContent/UploadFileModal.tsx | 13 +++- hooks/store/collections.tsx | 29 --------- hooks/store/links.tsx | 64 +------------------ hooks/store/tags.tsx | 20 ------ hooks/store/tokens.tsx | 20 ------ hooks/store/user.tsx | 11 ---- pages/settings/account.tsx | 16 +++++ pages/settings/password.tsx | 16 ++++- pages/settings/preference.tsx | 17 ++++- pages/tags/[id].tsx | 43 ++++++++++--- 24 files changed, 292 insertions(+), 203 deletions(-) diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index 8abc080..e83d388 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -155,15 +155,22 @@ const CollectionListing = () => { const updatedCollectionOrder = [...user.collectionOrder]; if (source.parentId !== destination.parentId) { - await updateCollection.mutateAsync({ - ...movedCollection, - parentId: - destination.parentId && destination.parentId !== "root" - ? Number(destination.parentId) - : destination.parentId === "root" - ? "root" - : null, - } as any); + await updateCollection.mutateAsync( + { + ...movedCollection, + parentId: + destination.parentId && destination.parentId !== "root" + ? Number(destination.parentId) + : destination.parentId === "root" + ? "root" + : null, + }, + { + onError: (error) => { + toast.error(error.message); + }, + } + ); } if ( diff --git a/components/LinkListOptions.tsx b/components/LinkListOptions.tsx index 6ec063d..8a2c0fb 100644 --- a/components/LinkListOptions.tsx +++ b/components/LinkListOptions.tsx @@ -10,6 +10,7 @@ import { useRouter } from "next/router"; import useLinkStore from "@/store/links"; import { Sort, ViewMode } from "@/types/global"; import { useBulkDeleteLinks, useLinks } from "@/hooks/store/links"; +import toast from "react-hot-toast"; type Props = { children: React.ReactNode; @@ -76,11 +77,20 @@ const LinkListOptions = ({ }; const bulkDeleteLinks = async () => { + const load = toast.loading(t("deleting")); + await deleteLinksById.mutateAsync( selectedLinks.map((link) => link.id as number), { - onSuccess: () => { - setSelectedLinks([]); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + setSelectedLinks([]); + toast.success(t("deleted")); + } }, } ); diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index 6d55d85..d795f5e 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -11,6 +11,7 @@ import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; import { useUser } from "@/hooks/store/user"; import { useDeleteLink, useUpdateLink } from "@/hooks/store/links"; +import toast from "react-hot-toast"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -44,12 +45,29 @@ export default function LinkActions({ const deleteLink = useDeleteLink(); const pinLink = async () => { - const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0]; + const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0] ? true : false; - await updateLink.mutateAsync({ - ...link, - pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }], - }); + const load = toast.loading(t("updating")); + + await updateLink.mutateAsync( + { + ...link, + pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }], + }, + { + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + toast.success( + isAlreadyPinned ? t("link_unpinned") : t("link_pinned") + ); + } + }, + } + ); }; return ( @@ -136,7 +154,21 @@ export default function LinkActions({ onClick={async (e) => { (document?.activeElement as HTMLElement)?.blur(); e.shiftKey - ? await deleteLink.mutateAsync(link.id as number) + ? async () => { + const load = toast.loading(t("deleting")); + + await deleteLink.mutateAsync(link.id as number, { + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + toast.success(t("deleted")); + } + }, + }); + } : setDeleteLinkModal(true); }} > diff --git a/components/ModalContent/BulkDeleteLinksModal.tsx b/components/ModalContent/BulkDeleteLinksModal.tsx index c97553e..22d402b 100644 --- a/components/ModalContent/BulkDeleteLinksModal.tsx +++ b/components/ModalContent/BulkDeleteLinksModal.tsx @@ -4,6 +4,7 @@ import Modal from "../Modal"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; import { useBulkDeleteLinks } from "@/hooks/store/links"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -16,12 +17,21 @@ export default function BulkDeleteLinksModal({ onClose }: Props) { const deleteLinksById = useBulkDeleteLinks(); const deleteLink = async () => { + const load = toast.loading(t("deleting")); + await deleteLinksById.mutateAsync( selectedLinks.map((link) => link.id as number), { - onSuccess: () => { - setSelectedLinks([]); - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + setSelectedLinks([]); + onClose(); + toast.success(t("deleted")); + } }, } ); diff --git a/components/ModalContent/BulkEditLinksModal.tsx b/components/ModalContent/BulkEditLinksModal.tsx index 3797e38..c2eab0f 100644 --- a/components/ModalContent/BulkEditLinksModal.tsx +++ b/components/ModalContent/BulkEditLinksModal.tsx @@ -36,6 +36,8 @@ export default function BulkEditLinksModal({ onClose }: Props) { if (!submitLoader) { setSubmitLoader(true); + const load = toast.loading(t("updating")); + await updateLinks.mutateAsync( { links: selectedLinks, @@ -43,9 +45,16 @@ export default function BulkEditLinksModal({ onClose }: Props) { removePreviousTags, }, { - onSuccess: () => { - setSelectedLinks([]); - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + setSelectedLinks([]); + onClose(); + toast.success(t("updated")); + } }, } ); diff --git a/components/ModalContent/DeleteCollectionModal.tsx b/components/ModalContent/DeleteCollectionModal.tsx index 907f1c0..359209e 100644 --- a/components/ModalContent/DeleteCollectionModal.tsx +++ b/components/ModalContent/DeleteCollectionModal.tsx @@ -7,6 +7,7 @@ import Modal from "../Modal"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; import { useDeleteCollection } from "@/hooks/store/collections"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -39,10 +40,19 @@ export default function DeleteCollectionModal({ setSubmitLoader(true); + const load = toast.loading(t("deleting_collection")); + deleteCollection.mutateAsync(collection.id as number, { - onSuccess: () => { - onClose(); - router.push("/collections"); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + router.push("/collections"); + toast.success(t("deleted")); + } }, }); diff --git a/components/ModalContent/DeleteLinkModal.tsx b/components/ModalContent/DeleteLinkModal.tsx index ba5e5ed..5a21075 100644 --- a/components/ModalContent/DeleteLinkModal.tsx +++ b/components/ModalContent/DeleteLinkModal.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/router"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; import { useDeleteLink } from "@/hooks/store/links"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -24,13 +25,21 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) { }, []); const submit = async () => { - await deleteLink.mutateAsync(link.id as number, { - onSuccess: () => { - if (router.pathname.startsWith("/links/[id]")) { - router.push("/dashboard"); - } + const load = toast.loading(t("deleting")); - onClose(); + await deleteLink.mutateAsync(link.id as number, { + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + if (router.pathname.startsWith("/links/[id]")) { + router.push("/dashboard"); + } + onClose(); + toast.success(t("deleted")); + } }, }); }; diff --git a/components/ModalContent/EditCollectionModal.tsx b/components/ModalContent/EditCollectionModal.tsx index 3009122..f8dfd20 100644 --- a/components/ModalContent/EditCollectionModal.tsx +++ b/components/ModalContent/EditCollectionModal.tsx @@ -5,6 +5,7 @@ import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useUpdateCollection } from "@/hooks/store/collections"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -29,9 +30,18 @@ export default function EditCollectionModal({ setSubmitLoader(true); + const load = toast.loading(t("updating_collection")); + await updateCollection.mutateAsync(collection, { - onSuccess: () => { - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + toast.success(t("updated")); + } }, }); diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index ae52ef8..84dae82 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -36,9 +36,18 @@ export default function EditCollectionSharingModal({ setSubmitLoader(true); + const load = toast.loading(t("updating_collection")); + await updateCollection.mutateAsync(collection, { - onSuccess: () => { - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + toast.success(t("updated")); + } }, }); diff --git a/components/ModalContent/EditLinkModal.tsx b/components/ModalContent/EditLinkModal.tsx index 7613c25..2d9711b 100644 --- a/components/ModalContent/EditLinkModal.tsx +++ b/components/ModalContent/EditLinkModal.tsx @@ -8,6 +8,7 @@ import Link from "next/link"; import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useUpdateLink } from "@/hooks/store/links"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -51,9 +52,18 @@ export default function EditLinkModal({ onClose, activeLink }: Props) { if (!submitLoader) { setSubmitLoader(true); + const load = toast.loading(t("updating")); + await updateLink.mutateAsync(link, { - onSuccess: () => { - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + toast.success(t("updated")); + } }, }); diff --git a/components/ModalContent/NewCollectionModal.tsx b/components/ModalContent/NewCollectionModal.tsx index 24284e2..df9d9bb 100644 --- a/components/ModalContent/NewCollectionModal.tsx +++ b/components/ModalContent/NewCollectionModal.tsx @@ -6,6 +6,7 @@ import Modal from "../Modal"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import { useTranslation } from "next-i18next"; import { useCreateCollection } from "@/hooks/store/collections"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -37,9 +38,18 @@ export default function NewCollectionModal({ onClose, parent }: Props) { setSubmitLoader(true); + const load = toast.loading(t("creating")); + await createCollection.mutateAsync(collection, { - onSuccess: () => { - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + toast.success(t("created")); + } }, }); diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 05350a5..660c1e4 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -10,6 +10,7 @@ import Modal from "../Modal"; import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useAddLink } from "@/hooks/store/links"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -88,9 +89,18 @@ export default function NewLinkModal({ onClose }: Props) { if (!submitLoader) { setSubmitLoader(true); + const load = toast.loading(t("creating_link")); + await addLink.mutateAsync(link, { - onSuccess: () => { - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + toast.success(t("link_created")); + } }, }); diff --git a/components/ModalContent/NewTokenModal.tsx b/components/ModalContent/NewTokenModal.tsx index 90e98a7..0528270 100644 --- a/components/ModalContent/NewTokenModal.tsx +++ b/components/ModalContent/NewTokenModal.tsx @@ -29,9 +29,17 @@ export default function NewTokenModal({ onClose }: Props) { if (!submitLoader) { setSubmitLoader(true); + const load = toast.loading(t("creating_token")); + await addToken.mutateAsync(token, { - onSuccess: (data) => { - setNewToken(data.secretKey); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + setNewToken(data.secretKey); + } }, }); diff --git a/components/ModalContent/RevokeTokenModal.tsx b/components/ModalContent/RevokeTokenModal.tsx index 0e128c9..6964d3d 100644 --- a/components/ModalContent/RevokeTokenModal.tsx +++ b/components/ModalContent/RevokeTokenModal.tsx @@ -4,6 +4,7 @@ import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; import { AccessToken } from "@prisma/client"; import { useRevokeToken } from "@/hooks/store/tokens"; +import toast from "react-hot-toast"; type Props = { onClose: Function; @@ -21,9 +22,18 @@ export default function DeleteTokenModal({ onClose, activeToken }: Props) { }, [activeToken]); const deleteLink = async () => { + const load = toast.loading(t("deleting")); + await revokeToken.mutateAsync(token.id, { - onSuccess: () => { - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + toast.success(t("token_revoked")); + } }, }); }; diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 2e43d56..bbb1577 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -116,11 +116,20 @@ export default function UploadFileModal({ onClose }: Props) { setSubmitLoader(true); + const load = toast.loading(t("creating")); + await uploadFile.mutateAsync( { link, file }, { - onSuccess: () => { - onClose(); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + onClose(); + toast.success(t("created_success")); + } }, } ); diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx index 4d0a93f..e16a326 100644 --- a/hooks/store/collections.tsx +++ b/hooks/store/collections.tsx @@ -1,7 +1,5 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; -import { useTranslation } from "next-i18next"; -import toast from "react-hot-toast"; const useCollections = () => { return useQuery({ @@ -15,13 +13,10 @@ const useCollections = () => { }; const useCreateCollection = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (body: any) => { - const load = toast.loading(t("creating")); - const response = await fetch("/api/v1/collections", { body: JSON.stringify(body), headers: { @@ -30,8 +25,6 @@ const useCreateCollection = () => { method: "POST", }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -39,25 +32,18 @@ const useCreateCollection = () => { return data.response; }, onSuccess: (data) => { - toast.success(t("created")); return queryClient.setQueryData(["collections"], (oldData: any) => { return [...oldData, data]; }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; const useUpdateCollection = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (body: any) => { - const load = toast.loading(t("updating_collection")); - const response = await fetch(`/api/v1/collections/${body.id}`, { method: "PUT", headers: { @@ -66,8 +52,6 @@ const useUpdateCollection = () => { body: JSON.stringify(body), }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -76,7 +60,6 @@ const useUpdateCollection = () => { }, onSuccess: (data) => { { - toast.success(t("updated")); return queryClient.setQueryData(["collections"], (oldData: any) => { return oldData.map((collection: any) => collection.id === data.id ? data : collection @@ -92,20 +75,14 @@ const useUpdateCollection = () => { // ) // }); // }, - onError: (error) => { - toast.error(error.message); - }, }); }; const useDeleteCollection = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (id: number) => { - const load = toast.loading(t("deleting_collection")); - const response = await fetch(`/api/v1/collections/${id}`, { method: "DELETE", headers: { @@ -113,8 +90,6 @@ const useDeleteCollection = () => { }, }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -122,14 +97,10 @@ const useDeleteCollection = () => { return data.response; }, onSuccess: (data) => { - toast.success(t("deleted")); return queryClient.setQueryData(["collections"], (oldData: any) => { return oldData.filter((collection: any) => collection.id !== data.id); }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; diff --git a/hooks/store/links.tsx b/hooks/store/links.tsx index 2fd7c2b..da503f4 100644 --- a/hooks/store/links.tsx +++ b/hooks/store/links.tsx @@ -5,14 +5,12 @@ import { useQueryClient, useMutation, } from "@tanstack/react-query"; -import { useEffect, useMemo } from "react"; +import { useMemo } from "react"; import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags, LinkRequestQuery, } from "@/types/global"; -import toast from "react-hot-toast"; -import { useTranslation } from "next-i18next"; import { useRouter } from "next/router"; const useLinks = (params: LinkRequestQuery = {}) => { @@ -98,13 +96,10 @@ const buildQueryString = (params: LinkRequestQuery) => { }; const useAddLink = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => { - const load = toast.loading(t("creating_link")); - const response = await fetch("/api/v1/links", { method: "POST", headers: { @@ -113,8 +108,6 @@ const useAddLink = () => { body: JSON.stringify(link), }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -122,8 +115,6 @@ const useAddLink = () => { return data.response; }, onSuccess: (data) => { - toast.success(t("link_created")); - queryClient.setQueryData(["dashboardData"], (oldData: any) => { if (!oldData) return undefined; return [data, ...oldData]; @@ -141,20 +132,14 @@ const useAddLink = () => { queryClient.invalidateQueries({ queryKey: ["tags"] }); queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; const useUpdateLink = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => { - const load = toast.loading(t("updating")); - const response = await fetch(`/api/v1/links/${link.id}`, { method: "PUT", headers: { @@ -163,8 +148,6 @@ const useUpdateLink = () => { body: JSON.stringify(link), }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -172,8 +155,6 @@ const useUpdateLink = () => { return data.response; }, onSuccess: (data) => { - toast.success(t("updated")); - queryClient.setQueryData(["dashboardData"], (oldData: any) => { if (!oldData) return undefined; return oldData.map((e: any) => (e.id === data.id ? data : e)); @@ -193,26 +174,18 @@ const useUpdateLink = () => { queryClient.invalidateQueries({ queryKey: ["tags"] }); queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; const useDeleteLink = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (id: number) => { - const load = toast.loading(t("deleting")); - const response = await fetch(`/api/v1/links/${id}`, { method: "DELETE", }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -220,8 +193,6 @@ const useDeleteLink = () => { return data.response; }, onSuccess: (data) => { - toast.success(t("deleted")); - queryClient.setQueryData(["dashboardData"], (oldData: any) => { if (!oldData) return undefined; return oldData.filter((e: any) => e.id !== data.id); @@ -241,9 +212,6 @@ const useDeleteLink = () => { queryClient.invalidateQueries({ queryKey: ["tags"] }); queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; @@ -281,13 +249,10 @@ const useGetLink = () => { }; const useBulkDeleteLinks = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (linkIds: number[]) => { - const load = toast.loading(t("deleting")); - const response = await fetch("/api/v1/links", { method: "DELETE", headers: { @@ -296,8 +261,6 @@ const useBulkDeleteLinks = () => { body: JSON.stringify({ linkIds }), }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -305,8 +268,6 @@ const useBulkDeleteLinks = () => { return linkIds; }, onSuccess: (data) => { - toast.success(t("deleted")); - queryClient.setQueryData(["dashboardData"], (oldData: any) => { if (!oldData) return undefined; return oldData.filter((e: any) => !data.includes(e.id)); @@ -326,14 +287,10 @@ const useBulkDeleteLinks = () => { queryClient.invalidateQueries({ queryKey: ["tags"] }); queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; const useUploadFile = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ @@ -354,8 +311,6 @@ const useUploadFile = () => { return { ok: false, data: "Invalid file type." }; } - const load = toast.loading(t("creating")); - const response = await fetch("/api/v1/links", { body: JSON.stringify({ ...link, @@ -385,13 +340,9 @@ const useUploadFile = () => { ); } - toast.dismiss(load); - return data.response; }, onSuccess: (data) => { - toast.success(t("created_success")); - queryClient.setQueryData(["dashboardData"], (oldData: any) => { if (!oldData) return undefined; return [data, ...oldData]; @@ -409,14 +360,10 @@ const useUploadFile = () => { queryClient.invalidateQueries({ queryKey: ["tags"] }); queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; const useBulkEditLinks = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ @@ -432,8 +379,6 @@ const useBulkEditLinks = () => { >; removePreviousTags: boolean; }) => { - const load = toast.loading(t("updating")); - const response = await fetch("/api/v1/links", { method: "PUT", headers: { @@ -442,8 +387,6 @@ const useBulkEditLinks = () => { body: JSON.stringify({ links, newData, removePreviousTags }), }); - toast.dismiss(load); - const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -451,8 +394,6 @@ const useBulkEditLinks = () => { return data.response; }, onSuccess: (data, { links, newData, removePreviousTags }) => { - toast.success(t("updated")); - queryClient.setQueryData(["dashboardData"], (oldData: any) => { if (!oldData) return undefined; return oldData.map((e: any) => @@ -477,9 +418,6 @@ const useBulkEditLinks = () => { queryClient.invalidateQueries({ queryKey: ["tags"] }); queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; diff --git a/hooks/store/tags.tsx b/hooks/store/tags.tsx index c9738fe..deab456 100644 --- a/hooks/store/tags.tsx +++ b/hooks/store/tags.tsx @@ -1,6 +1,4 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import toast from "react-hot-toast"; -import { useTranslation } from "next-i18next"; import { TagIncludingLinkCount } from "@/types/global"; const useTags = () => { @@ -18,12 +16,9 @@ const useTags = () => { const useUpdateTag = () => { const queryClient = useQueryClient(); - const { t } = useTranslation(); return useMutation({ mutationFn: async (tag: TagIncludingLinkCount) => { - const load = toast.loading(t("applying_changes")); - const response = await fetch(`/api/v1/tags/${tag.id}`, { body: JSON.stringify(tag), headers: { @@ -35,8 +30,6 @@ const useUpdateTag = () => { const data = await response.json(); if (!response.ok) throw new Error(data.response); - toast.dismiss(load); - return data.response; }, onSuccess: (data) => { @@ -45,22 +38,15 @@ const useUpdateTag = () => { tag.id === data.id ? data : tag ) ); - toast.success(t("tag_renamed")); - }, - onError: (error) => { - toast.error(error.message); }, }); }; const useRemoveTag = () => { const queryClient = useQueryClient(); - const { t } = useTranslation(); return useMutation({ mutationFn: async (tagId: number) => { - const load = toast.loading(t("applying_changes")); - const response = await fetch(`/api/v1/tags/${tagId}`, { method: "DELETE", }); @@ -68,18 +54,12 @@ const useRemoveTag = () => { const data = await response.json(); if (!response.ok) throw new Error(data.response); - toast.dismiss(load); - return data.response; }, onSuccess: (data, variables) => { queryClient.setQueryData(["tags"], (oldData: any) => oldData.filter((tag: TagIncludingLinkCount) => tag.id !== variables) ); - toast.success(t("tag_deleted")); - }, - onError: (error) => { - toast.error(error.message); }, }); }; diff --git a/hooks/store/tokens.tsx b/hooks/store/tokens.tsx index 18bcd9d..b48fce0 100644 --- a/hooks/store/tokens.tsx +++ b/hooks/store/tokens.tsx @@ -1,6 +1,4 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import toast from "react-hot-toast"; -import { useTranslation } from "next-i18next"; import { AccessToken } from "@prisma/client"; const useTokens = () => { @@ -19,12 +17,9 @@ const useTokens = () => { const useAddToken = () => { const queryClient = useQueryClient(); - const { t } = useTranslation(); return useMutation({ mutationFn: async (body: Partial) => { - const load = toast.loading(t("creating_token")); - const response = await fetch("/api/v1/tokens", { body: JSON.stringify(body), method: "POST", @@ -33,8 +28,6 @@ const useAddToken = () => { const data = await response.json(); if (!response.ok) throw new Error(data.response); - toast.dismiss(load); - return data.response; }, onSuccess: (data) => { @@ -42,22 +35,15 @@ const useAddToken = () => { ...oldData, data.token, ]); - toast.success(t("token_added")); - }, - onError: (error) => { - toast.error(error.message); }, }); }; const useRevokeToken = () => { const queryClient = useQueryClient(); - const { t } = useTranslation(); return useMutation({ mutationFn: async (tokenId: number) => { - const load = toast.loading(t("deleting")); - const response = await fetch(`/api/v1/tokens/${tokenId}`, { method: "DELETE", }); @@ -65,18 +51,12 @@ const useRevokeToken = () => { const data = await response.json(); if (!response.ok) throw new Error(data.response); - toast.dismiss(load); - return data.response; }, onSuccess: (data, variables) => { queryClient.setQueryData(["tokens"], (oldData: AccessToken[]) => oldData.filter((token: Partial) => token.id !== variables) ); - toast.success(t("token_revoked")); - }, - onError: (error) => { - toast.error(error.message); }, }); }; diff --git a/hooks/store/user.tsx b/hooks/store/user.tsx index a470cc5..98ba03c 100644 --- a/hooks/store/user.tsx +++ b/hooks/store/user.tsx @@ -1,6 +1,4 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import toast from "react-hot-toast"; -import { useTranslation } from "next-i18next"; import { useSession } from "next-auth/react"; const useUser = () => { @@ -24,13 +22,10 @@ const useUser = () => { }; const useUpdateUser = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (user: any) => { - const load = toast.loading(t("applying_settings")); - const response = await fetch(`/api/v1/users/${user.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, @@ -39,14 +34,11 @@ const useUpdateUser = () => { const data = await response.json(); - toast.dismiss(load); - if (!response.ok) throw new Error(data.response); return data; }, onSuccess: (data) => { - toast.success(t("settings_applied")); queryClient.setQueryData(["user"], data.response); }, onMutate: async (user) => { @@ -55,9 +47,6 @@ const useUpdateUser = () => { return { ...oldData, ...user }; }); }, - onError: (error) => { - toast.error(error.message); - }, }); }; diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index c392596..06d1172 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -80,6 +80,8 @@ export default function Account() { const submit = async (password?: string) => { setSubmitLoader(true); + const load = toast.loading(t("applying_settings")); + await updateUser.mutateAsync( { ...user, @@ -92,6 +94,20 @@ export default function Account() { setEmailChangeVerificationModal(false); } }, + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + if (data.response.email !== user.email) { + toast.success(t("email_change_request")); + setEmailChangeVerificationModal(false); + } + + toast.success(t("settings_applied")); + } + }, } ); diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx index 4b0d3e8..808be7a 100644 --- a/pages/settings/password.tsx +++ b/pages/settings/password.tsx @@ -24,6 +24,8 @@ export default function Password() { setSubmitLoader(true); + const load = toast.loading(t("applying_settings")); + await updateUser.mutateAsync( { ...account, @@ -31,9 +33,17 @@ export default function Password() { oldPassword, }, { - onSuccess: () => { - setNewPassword(""); - setOldPassword(""); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + setNewPassword(""); + setOldPassword(""); + + toast.success(t("settings_applied")); + } }, } ); diff --git a/pages/settings/preference.tsx b/pages/settings/preference.tsx index 2e33ca1..4d93d02 100644 --- a/pages/settings/preference.tsx +++ b/pages/settings/preference.tsx @@ -74,7 +74,22 @@ export default function Appearance() { const submit = async () => { setSubmitLoader(true); - await updateUser.mutateAsync({ ...user }); + const load = toast.loading(t("applying_settings")); + + await updateUser.mutateAsync( + { ...user }, + { + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + toast.success(t("settings_applied")); + } + }, + } + ); setSubmitLoader(false); }; diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index e71fe84..1d00b81 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -11,6 +11,7 @@ import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; import { useRemoveTag, useTags, useUpdateTag } from "@/hooks/store/tags"; import Links from "@/components/LinkViews/Links"; +import toast from "react-hot-toast"; export default function Index() { const { t } = useTranslation(); @@ -74,11 +75,27 @@ export default function Index() { setSubmitLoader(true); - if (activeTag && newTagName) - await updateTag.mutateAsync({ - ...activeTag, - name: newTagName, - }); + if (activeTag && newTagName) { + const load = toast.loading(t("applying_changes")); + + await updateTag.mutateAsync( + { + ...activeTag, + name: newTagName, + }, + { + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + toast.success(t("tag_renamed")); + } + }, + } + ); + } setSubmitLoader(false); setRenameTag(false); @@ -87,12 +104,22 @@ export default function Index() { const remove = async () => { setSubmitLoader(true); - if (activeTag?.id) + if (activeTag?.id) { + const load = toast.loading(t("applying_changes")); + await removeTag.mutateAsync(activeTag?.id, { - onSuccess: () => { - router.push("/links"); + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + router.push("/links"); + toast.success(t("tag_deleted")); + } }, }); + } setSubmitLoader(false); setRenameTag(false); From 8031432995961f443cef7d38551f10d9b12bf433 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Wed, 14 Aug 2024 16:44:07 -0400 Subject: [PATCH 23/33] bug fix --- components/InstallApp.tsx | 2 +- components/ModalContent/DeleteCollectionModal.tsx | 2 +- components/ModalContent/DeleteLinkModal.tsx | 2 +- hooks/store/admin/users.tsx | 4 ++++ hooks/store/collections.tsx | 4 ++++ hooks/store/dashboardData.tsx | 4 ++++ hooks/store/links.tsx | 4 ++++ hooks/store/tags.tsx | 4 ++++ hooks/store/tokens.tsx | 4 ++++ hooks/store/user.tsx | 4 ++-- hooks/useInitialData.tsx | 10 ---------- hooks/useReorderCollection.tsx | 0 pages/collections/[id].tsx | 2 +- pages/links/index.tsx | 2 +- pages/links/pinned.tsx | 2 +- pages/public/collections/[id].tsx | 4 +++- pages/settings/account.tsx | 7 ++----- pages/tags/[id].tsx | 2 +- public/locales/en/common.json | 1 + 19 files changed, 39 insertions(+), 25 deletions(-) delete mode 100644 hooks/useReorderCollection.tsx diff --git a/components/InstallApp.tsx b/components/InstallApp.tsx index 4b70309..f191f00 100644 --- a/components/InstallApp.tsx +++ b/components/InstallApp.tsx @@ -8,7 +8,7 @@ const InstallApp = (props: Props) => { const [isOpen, setIsOpen] = useState(true); return isOpen && !isPWA() ? ( -
+
{ + const { status } = useSession(); + return useQuery({ queryKey: ["users"], queryFn: async () => { @@ -17,6 +20,7 @@ const useUsers = () => { const data = await response.json(); return data.response; }, + enabled: status === "authenticated", }); }; diff --git a/hooks/store/collections.tsx b/hooks/store/collections.tsx index e16a326..efa3809 100644 --- a/hooks/store/collections.tsx +++ b/hooks/store/collections.tsx @@ -1,7 +1,10 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; +import { useSession } from "next-auth/react"; const useCollections = () => { + const { status } = useSession(); + return useQuery({ queryKey: ["collections"], queryFn: async (): Promise => { @@ -9,6 +12,7 @@ const useCollections = () => { const data = await response.json(); return data.response; }, + enabled: status === "authenticated", }); }; diff --git a/hooks/store/dashboardData.tsx b/hooks/store/dashboardData.tsx index 358b035..d808ffd 100644 --- a/hooks/store/dashboardData.tsx +++ b/hooks/store/dashboardData.tsx @@ -1,7 +1,10 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { useQuery } from "@tanstack/react-query"; +import { useSession } from "next-auth/react"; const useDashboardData = () => { + const { status } = useSession(); + return useQuery({ queryKey: ["dashboardData"], queryFn: async (): Promise => { @@ -10,6 +13,7 @@ const useDashboardData = () => { return data.response; }, + enabled: status === "authenticated", }); }; diff --git a/hooks/store/links.tsx b/hooks/store/links.tsx index da503f4..4702ff4 100644 --- a/hooks/store/links.tsx +++ b/hooks/store/links.tsx @@ -12,6 +12,7 @@ import { LinkRequestQuery, } from "@/types/global"; import { useRouter } from "next/router"; +import { useSession } from "next-auth/react"; const useLinks = (params: LinkRequestQuery = {}) => { const router = useRouter(); @@ -58,6 +59,8 @@ const useLinks = (params: LinkRequestQuery = {}) => { }; const useFetchLinks = (params: string) => { + const { status } = useSession(); + return useInfiniteQuery({ queryKey: ["links", { params }], queryFn: async (params) => { @@ -80,6 +83,7 @@ const useFetchLinks = (params: string) => { } return lastPage.at(-1).id; }, + enabled: status === "authenticated", }); }; diff --git a/hooks/store/tags.tsx b/hooks/store/tags.tsx index deab456..a2c0b7d 100644 --- a/hooks/store/tags.tsx +++ b/hooks/store/tags.tsx @@ -1,7 +1,10 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { TagIncludingLinkCount } from "@/types/global"; +import { useSession } from "next-auth/react"; const useTags = () => { + const { status } = useSession(); + return useQuery({ queryKey: ["tags"], queryFn: async () => { @@ -11,6 +14,7 @@ const useTags = () => { const data = await response.json(); return data.response; }, + enabled: status === "authenticated", }); }; diff --git a/hooks/store/tokens.tsx b/hooks/store/tokens.tsx index b48fce0..35ac345 100644 --- a/hooks/store/tokens.tsx +++ b/hooks/store/tokens.tsx @@ -1,7 +1,10 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { AccessToken } from "@prisma/client"; +import { useSession } from "next-auth/react"; const useTokens = () => { + const { status } = useSession(); + return useQuery({ queryKey: ["tokens"], queryFn: async () => { @@ -12,6 +15,7 @@ const useTokens = () => { const data = await response.json(); return data.response as AccessToken[]; }, + enabled: status === "authenticated", }); }; diff --git a/hooks/store/user.tsx b/hooks/store/user.tsx index 98ba03c..200fd45 100644 --- a/hooks/store/user.tsx +++ b/hooks/store/user.tsx @@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; const useUser = () => { - const { data } = useSession(); + const { data, status } = useSession(); const userId = data?.user.id; @@ -16,7 +16,7 @@ const useUser = () => { return data.response; }, - enabled: !!userId, + enabled: !!userId && status === "authenticated", placeholderData: {}, }); }; diff --git a/hooks/useInitialData.tsx b/hooks/useInitialData.tsx index 04cf247..21832d1 100644 --- a/hooks/useInitialData.tsx +++ b/hooks/useInitialData.tsx @@ -1,24 +1,14 @@ import { useEffect } from "react"; import { useSession } from "next-auth/react"; import useLocalSettingsStore from "@/store/localSettings"; -import { useUser } from "./store/user"; export default function useInitialData() { const { status, data } = useSession(); - // const { setLinks } = useLinkStore(); - const { data: user = {} } = useUser(); const { setSettings } = useLocalSettingsStore(); useEffect(() => { setSettings(); }, [status, data]); - // Get the rest of the data - useEffect(() => { - if (user.id && (!process.env.NEXT_PUBLIC_STRIPE || user.username)) { - // setLinks(); - } - }, [user]); - return status; } diff --git a/hooks/useReorderCollection.tsx b/hooks/useReorderCollection.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index c96f807..5ff146f 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -322,7 +322,7 @@ export default function Index() { placeholderCount={1} useData={data} /> - {!data.isLoading && !links[0] && } + {!data.isLoading && links && !links[0] && }
{activeCollection && ( <> diff --git a/pages/links/index.tsx b/pages/links/index.tsx index 7b1e539..582e962 100644 --- a/pages/links/index.tsx +++ b/pages/links/index.tsx @@ -58,7 +58,7 @@ export default function Index() { placeholderCount={1} useData={data} /> - {!data.isLoading && !links[0] && ( + {!data.isLoading && links && !links[0] && ( )}
diff --git a/pages/links/pinned.tsx b/pages/links/pinned.tsx index 3f59c44..9d1fb53 100644 --- a/pages/links/pinned.tsx +++ b/pages/links/pinned.tsx @@ -53,7 +53,7 @@ export default function PinnedLinks() { placeholderCount={1} useData={data} /> - {!data.isLoading && !links[0] && ( + {!data.isLoading && links && !links[0] && (
- {!data.isLoading && !links[0] &&

{t("collection_is_empty")}

} + {!data.isLoading && links && !links[0] && ( +

{t("collection_is_empty")}

+ )} {/*

List created with Linkwarden. diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx index 06d1172..ded4683 100644 --- a/pages/settings/account.tsx +++ b/pages/settings/account.tsx @@ -203,17 +203,14 @@ export default function Account() {

{t("language")}

{t("from_html")} {t("from_wallabag")}
{t("edit")} -
    +
    • -
@@ -341,13 +346,14 @@ export default function Account() { {t("import_links")} -
    +
-
    +
    • {t("rename_tag")}
      @@ -215,6 +216,7 @@ export default function Index() { (document?.activeElement as HTMLElement)?.blur(); remove(); }} + className="whitespace-nowrap" > {t("delete_tag")}
From 5baf55694cd1a6d5b34918660cde6ed76b06eb82 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Wed, 14 Aug 2024 19:23:51 -0400 Subject: [PATCH 27/33] minor improvement --- components/SidebarHighlightLink.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/SidebarHighlightLink.tsx b/components/SidebarHighlightLink.tsx index d3f3647..a02fc1c 100644 --- a/components/SidebarHighlightLink.tsx +++ b/components/SidebarHighlightLink.tsx @@ -14,6 +14,7 @@ export default function SidebarHighlightLink({ return (
-

{title}

+

{title}

From 7bd0e29538418ec7b0725b6affd25e972b6e9d56 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Wed, 14 Aug 2024 20:07:06 -0400 Subject: [PATCH 28/33] small improvement --- pages/confirmation.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pages/confirmation.tsx b/pages/confirmation.tsx index 923c935..8d28aaf 100644 --- a/pages/confirmation.tsx +++ b/pages/confirmation.tsx @@ -43,6 +43,12 @@ export default function EmailConfirmaion() {
+ {router.query.email && typeof router.query.email === "string" && ( +

+ {decodeURIComponent(router.query.email)} +

+ )} +

{t("verification_email_sent_desc")}

From 04d2b3c6b25374cb3eaa7956b29c0972dbf61809 Mon Sep 17 00:00:00 2001 From: shichen437 Date: Thu, 15 Aug 2024 17:20:46 +0800 Subject: [PATCH 29/33] feat(lang): add chinese translate --- next-i18next.config.js | 2 +- public/locales/zh/common.json | 374 ++++++++++++++++++++++++++++++++++ 2 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 public/locales/zh/common.json diff --git a/next-i18next.config.js b/next-i18next.config.js index efdf922..7855244 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -2,7 +2,7 @@ module.exports = { i18n: { defaultLocale: "en", - locales: ["en", "it", "fr"], + locales: ["en", "it", "fr", "zh"], }, reloadOnPrerender: process.env.NODE_ENV === "development", }; diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json new file mode 100644 index 0000000..b5b7977 --- /dev/null +++ b/public/locales/zh/common.json @@ -0,0 +1,374 @@ +{ + "user_administration": "用户管理", + "search_users": "搜索用户", + "no_users_found": "未找到用户。", + "no_user_found_in_search": "在给定的搜索查询中未找到用户。", + "username": "用户名", + "email": "邮箱", + "subscribed": "订阅", + "created_at": "创建时间", + "not_available": "N/A", + "check_your_email": "检查你的邮箱", + "authenticating": "身份验证中...", + "verification_email_sent": "发送验证邮件。", + "verification_email_sent_desc": "登录链接已发送到您的邮箱。如果没有看到邮件,请检查你的垃圾邮件文件夹。", + "resend_email": "重新发送邮件", + "invalid_credentials": "无效凭证", + "fill_all_fields": "请填写所有字段", + "enter_credentials": "账号登录", + "username_or_email": "用户名或邮箱", + "password": "密码", + "confirm_password": "确认密码", + "forgot_password": "忘记密码?", + "login": "登录", + "or_continue_with": "第三方登录", + "new_here": "新用户?", + "sign_up": "注册", + "sign_in_to_your_account": "登录你的账户", + "dashboard_desc": "你的数据概览", + "link": "链接", + "links": "链接", + "collection": "收藏夹", + "collections": "收藏夹", + "tag": "标签", + "tags": "标签", + "recent": "最近", + "recent_links_desc": "最近添加的链接", + "view_all": "查看全部", + "view_added_links_here": "查看最近添加的链接!", + "view_added_links_here_desc": "此部分将查看您有权访问的每个收藏夹中最新添加的链接。", + "add_link": "添加新链接", + "import_links": "导入链接", + "from_linkwarden": "从 Linkwarden", + "from_html": "从书签 HTML 文件", + "from_wallabag": "从 Wallabag(JSON 文件)", + "pinned": "置顶", + "pinned_links_desc": "你置顶的链接", + "pin_favorite_links_here": "在这里置顶您最喜欢的链接!", + "pin_favorite_links_here_desc": "您可以通过点击每个链接上的三个点,然后点击“置顶到仪表板”来置顶您最喜欢的链接。", + "sending_password_link": "发送重置密码链接", + "password_email_prompt": "输入你的邮箱,我们会给你发送一个重置密码的链接。", + "send_reset_link": "发送重置链接", + "reset_email_sent_desc": "检查你的邮箱以获取重置密码的链接。如果它在几分钟内没有出现,请检查你的垃圾邮件文件夹。", + "back_to_login": "返回登录", + "email_sent": "邮件已发送!", + "passwords_mismatch": "密码错误。", + "password_too_short": "密码需至少 8 个字符。", + "creating_account": "正在创建账户...", + "account_created": "创建账户成功!", + "trial_offer_desc": "免费解锁 {{count}} 天的高级服务!", + "register_desc": "创建新的账户", + "registration_disabled_desc": "此实例已禁用注册,如有任何问题,请与管理员联系。", + "enter_details": "输入您的详细信息", + "display_name": "昵称", + "sign_up_agreement": "注册即表示您同意遵守我们的 <0>服务条款 和 <1>隐私政策。", + "need_help": "需要帮助吗?", + "get_in_touch": "保持联系", + "already_registered": "已有账户?", + "deleting_selections": "正在删除选中的链接...", + "links_deleted": "{{count}} 个链接已删除。", + "link_deleted": "1 个链接已删除。", + "links_selected": "{{count}} 个链接已选中。", + "link_selected": "1 个链接已选中。", + "nothing_selected": "未选中任何内容", + "edit": "编辑", + "delete": "删除", + "nothing_found": "沒有找到任何内容。", + "redirecting_to_stripe": "重定向到 Stripe...", + "subscribe_title": "订阅 Linkwarden!", + "subscribe_desc": "你将跳转到 Stripe,如有任何问题请随时联系我们 <0>support@linkwarden.app 。", + "monthly": "每月", + "yearly": "每年", + "discount_percent": "{{percent}}% 折扣", + "billed_monthly": "月付", + "billed_yearly": "年付", + "total": "总计", + "total_annual_desc": "{{count}}-天免费体验,然后 ${{annualPrice}} 每年", + "total_monthly_desc": "{{count}}-天免费体验,然后 ${{monthlyPrice}} 每月", + "plus_tax": "+增值税(如适用)", + "complete_subscription": "完成订阅", + "sign_out": "退出", + "access_tokens": "Access Tokens", + "access_tokens_description": "Access Tokens 可用于从其他应用程序和服务访问 Linkwarden,而无需泄露您的用户名和密码。", + "new_token": "新 Access Token", + "name": "名称", + "created_success": "创建成功!", + "created": "已创建", + "expires": "过期", + "accountSettings": "账户设置", + "language": "语言", + "profile_photo": "头像", + "upload_new_photo": "上传新头像", + "remove_photo": "移除照片", + "make_profile_private": "将个人资料设为私密", + "profile_privacy_info": "这将限制谁可以找到你并将你添加到新收藏夹。", + "whitelisted_users": "白名单用户", + "whitelisted_users_info": "请提供您希望允许查看您个人资料的用户的用户名,以逗号分隔。", + "whitelisted_users_placeholder": "你的资料现在对所有人都是隐藏的。", + "save_changes": "保存修改", + "import_export": "导入 & 导出", + "import_data": "从其它平台导入", + "download_data": "下载数据", + "export_data": "导出数据", + "delete_account": "删除账户", + "delete_account_warning": "这将永久删除您拥有的所有链接、收藏夹、标签和存档数据。", + "cancel_subscription_notice": "它将取消你的订阅。", + "account_deletion_page": "账户删除页面", + "applying_settings": "应用设置中...", + "settings_applied": "已应用设置!", + "email_change_request": "已发送电子邮件更改请求。请确认新的邮箱地址。", + "image_upload_size_error": "请选择小于 1 MB的 PNG 或 JPEG 文件。", + "image_upload_format_error": "无效的文件格式。", + "importing_bookmarks": "正在导入书签...", + "import_success": "已导入书签!请刷新页面。", + "more_coming_soon": "即将推出...", + "billing_settings": "账单设置", + "manage_subscription_intro": "要管理/取消订阅,请访问", + "billing_portal": "账单管理", + "help_contact_intro": "如果您仍然需要帮助或遇到任何问题,请随时与我们联系:", + "fill_required_fields": "请填写必填字段。", + "deleting_message": "删除所有内容,请稍等...", + "delete_warning": "这将永久删除您拥有的所有链接、收藏夹、标签和存档数据。它还将注销您的账户。此操作不可逆!", + "optional": "可选", + "feedback_help": "(但它确实能帮助我们改善!)", + "reason_for_cancellation": "取消原因", + "please_specify": "请指定", + "customer_service": "客户服务", + "low_quality": "低质量", + "missing_features": "缺失功能", + "switched_service": "选择服务", + "too_complex": "太复杂", + "too_expensive": "太昂贵", + "unused": "从未使用", + "other": "其它", + "more_information": "更多信息(越详细,越有帮助)", + "feedback_placeholder": "例如,我需要一个功能...", + "delete_your_account": "删除账户", + "change_password": "修改密码", + "password_length_error": "密码需至少 8 个字符。", + "applying_changes": "正在申请...", + "password_change_instructions": "要更改您的密码,请填写以下内容。您的密码至少应为 8 个字符。", + "old_password": "旧密码", + "new_password": "新密码", + "preference": "首选项", + "select_theme": "选择主题", + "dark": "Dark", + "light": "Light", + "archive_settings": "存档设置", + "formats_to_archive": "存档/保存网页格式:", + "screenshot": "截图", + "pdf": "PDF", + "archive_org_snapshot": "Archive.org 快照", + "link_settings": "链接设置", + "prevent_duplicate_links": "防止重复链接", + "clicking_on_links_should": "点击链接:", + "open_original_content": "打开原始内容", + "open_pdf_if_available": "打开PDF,如果有的话", + "open_readable_if_available": "打开可读视图(如果可用)", + "open_screenshot_if_available": "打开屏幕截图,如果有的话", + "open_webpage_if_available": "打开网页副本,如果有的话", + "tag_renamed": "标签已重命名!", + "tag_deleted": "标签已删除!", + "rename_tag": "重命名标签", + "delete_tag": "删除标签", + "list_created_with_linkwarden": "使用 Linkwarden 创建的列表", + "by_author": "作者 {{author}}", + "by_author_and_other": "作者:{{author}} 和 {{count}} 其他人", + "by_author_and_others": "作者: {{author}} 和 {{count}} 其他人", + "search_count_link": "搜索 {{count}} 个链接", + "search_count_links": "搜索 {{count}} 个链接", + "collection_is_empty": "此收藏夹为空", + "all_links": "全部链接", + "all_links_desc": "所有收藏夹的链接", + "you_have_not_added_any_links": "你还没有创建任何链接", + "collections_you_own": "你的收藏夹", + "new_collection": "新收藏夹", + "other_collections": "其它收藏夹", + "other_collections_desc": "您所属的共享收藏夹", + "showing_count_results": "展示 {{count}} 条结果", + "showing_count_result": "展示 {{count}} 条结果", + "edit_collection_info": "编辑收藏夹信息", + "share_and_collaborate": "共享和协作", + "view_team": "查看团队", + "team": "团队", + "create_subcollection": "创建子收藏夹", + "delete_collection": "删除收藏夹", + "leave_collection": "离开收藏夹", + "email_verified_signing_out": "邮箱已验证。正在退出...", + "invalid_token": "无效 token", + "sending_password_recovery_link": "发送密码重置链接...", + "please_fill_all_fields": "请填写所有字段。", + "password_updated": "密码已修改!", + "reset_password": "重置密码", + "enter_email_for_new_password": "输入您的邮箱,以便我们向您发送创建新密码的链接。", + "update_password": "更新密码", + "password_successfully_updated": "您的密码已成功更新。", + "user_already_member": "用户已存在。", + "you_are_already_collection_owner": "您已经是收藏夹的所有者。", + "date_newest_first": "日期 (最新)", + "date_oldest_first": "日期 (最早)", + "name_az": "名字 (A-Z)", + "name_za": "名字 (Z-A)", + "description_az": "描述 (A-Z)", + "description_za": "描述 (Z-A)", + "all_rights_reserved": "© {{date}} <0>Linkwarden 版权所有", + "you_have_no_collections": "你还没有收藏夹...", + "you_have_no_tags": "你还没有标签...", + "cant_change_collection_you_dont_own": "您无法对不属于您的收藏夹进行更改。", + "account": "账户", + "billing": "帐单", + "linkwarden_version": "Linkwarden {{version}}", + "help": "帮助", + "github": "GitHub", + "twitter": "Twitter", + "mastodon": "Mastodon", + "link_preservation_in_queue": "链接保存正在处理中...", + "check_back_later": "请稍后再查看结果", + "there_are_more_formats": "队列中有更多已保存的格式。", + "settings": "设置", + "switch_to": "切换到 {{theme}}", + "logout": "注销", + "start_journey": "通过创建一个新的链接开始你的旅程!", + "create_new_link": "创建新链接", + "new_link": "新链接", + "create_new": "新建", + "pwa_install_prompt": "安装 Linkwarden 到您的主屏幕,以获得更快的访问和增强的体验。 <0>了解更多", + "full_content": "完整内容", + "slower": "较慢", + "new_version_announcement": "查看 <0>Linkwarden {{version}} 了解新功能。", + "creating": "正在创建...", + "upload_file": "上传文件", + "file": "文件", + "file_types": "PDF,PNG,JPG (不超过 {{size}} MB)", + "description": "描述", + "auto_generated": "若未提供将自动生成。", + "example_link": "例如:示例链接", + "hide": "隐藏", + "more": "更多", + "options": "选项", + "description_placeholder": "笔记,想法等等。", + "deleting": "正在删除...", + "token_revoked": "Token 已撤销。", + "revoke_token": "撤销 Token", + "revoke_confirmation": "确定要撤销此 Access Token 吗?任何使用此 token 的应用或服务将无法再使用它访问 Linkwarden。", + "revoke": "撤销", + "sending_request": "正在发送请求...", + "link_being_archived": "链接正在归档...", + "preserved_formats": "保存格式", + "available_formats": "以下格式可用于此链接:", + "readable": "可读视图", + "preservation_in_queue": "链接保存正在处理中...", + "view_latest_snapshot": "在 archive.org 上查看最新快照", + "refresh_preserved_formats": "刷新保留格式", + "this_deletes_current_preservations": "这将删除当前保存", + "create_new_user": "创建新用户", + "placeholder_johnny": "Johnny", + "placeholder_email": "johnny@example.com", + "placeholder_john": "john", + "user_created": "用户已创建", + "fill_all_fields_error": "请填写所有的字段。", + "password_change_note": "<0>注意: 请务必通知用户他们需要更改密码。", + "create_user": "创建用户", + "creating_token": "创建 Token 中...", + "token_created": "Token 创建完成", + "access_token_created": "Access Token 已创建", + "token_creation_notice": "你的新 token 已创建。 请复制并保存在安全的地方。您将无法再看到它。", + "copied_to_clipboard": "已复制到剪贴板", + "copy_to_clipboard": "复制到剪贴板", + "create_access_token": "创建一个 Access Token", + "expires_in": "有效期:", + "token_name_placeholder": "例如:iOS 快捷方式", + "create_token": "创建 Access Token", + "7_days": "7 天", + "30_days": "30 天", + "60_days": "60 天", + "90_days": "90 天", + "no_expiration": "永久", + "creating_link": "正在创建链接...", + "link_created": "链接已创建!", + "link_name_placeholder": "若留空将自动生成。", + "link_url_placeholder": "示例: http://example.com/", + "link_description_placeholder": "笔记,想法等等。", + "more_options": "更多选项", + "hide_options": "隐藏选项", + "create_link": "创建链接", + "new_sub_collection": "新子收藏夹", + "for_collection": "对于 {{name}}", + "create_new_collection": "创建新收藏夹", + "color": "颜色", + "reset": "重置", + "collection_name_placeholder": "例如:示例收藏夹", + "collection_description_placeholder": "该收藏夹用于...", + "create_collection_button": "创建收藏夹", + "password_change_warning": "更改邮箱前,请确认密码。", + "stripe_update_note": " 更新此字段也会更改 Stripe 上的账单邮箱。", + "sso_will_be_removed_warning": "如果您更改电子邮件地址,任何现有的 {{service}} SSO 连接都将被删除。", + "old_email": "旧邮箱", + "new_email": "新邮箱", + "confirm": "确认", + "edit_link": "编辑链接", + "updating": "正在更新...", + "updated": "已更新", + "placeholder_example_link": "例如:示例链接", + "make_collection_public": "公开收藏夹", + "make_collection_public_checkbox": "将其设为公开收藏夹", + "make_collection_public_desc": "这将允许任何人查看该收藏夹及其用户。", + "sharable_link_guide": "可分享的链接(点击复制)", + "copied": "已复制", + "members": "成员", + "members_username_placeholder": "用户名(不带'@')", + "owner": "所有者", + "admin": "管理员", + "contributor": "Contributor", + "viewer": "Viewer", + "viewer_desc": "只读访问", + "contributor_desc": "可以查看和创建链接", + "admin_desc": "完全访问所有链接", + "remove_member": "移除成员", + "placeholder_example_collection": "例如:示例收藏夹", + "placeholder_collection_purpose": "此收藏夹的目的...", + "deleting_user": "正在删除用户...", + "user_deleted": "用户已删除", + "delete_user": "删除用户", + "confirm_user_deletion": "确定删除该用户吗?", + "irreversible_action_warning": "此操作不可逆!", + "delete_confirmation": "删除,我知道我在做什么。", + "delete_link": "删除链接", + "deleted": "已删除", + "link_deletion_confirmation_message": "要删除此链接吗?", + "warning": "警告", + "irreversible_warning": "此操作不可逆!", + "shift_key_tip": "按住 Shift 键并单击“删除”即可在以后绕过此确认。", + "deleting_collection": "删除中...", + "collection_deleted": "收藏夹已删除。", + "confirm_deletion_prompt": "请确认, 在下面的输入框中输入 \"{{name}}\" :", + "type_name_placeholder": "在此输入 \"{{name}}\" 。", + "deletion_warning": "删除此收藏夹将永久清除其所有内容,并且所有人都将无法访问,包括之前具有访问权限的成员。", + "leave_prompt": "单击下面的按钮离开当前收藏夹。", + "leave": "离开", + "edit_links": "编辑 {{count}} 链接", + "move_to_collection": "移至收藏夹", + "add_tags": "添加标签", + "remove_previous_tags": "删除以前的标签", + "delete_links": "删除 {{count}} 个链接", + "links_deletion_confirmation_message": "确定要删除 {{count}} 个链接吗?", + "warning_irreversible": "警告:此操作不可逆!", + "shift_key_instruction": "按住“Shift”键并单击“删除”即可在以后绕过此确认。", + "link_selection_error": "您无权编辑或删除此项目。", + "no_description": "未提供描述", + "applying": "正在申请...", + "unpin": "取消置顶", + "pin_to_dashboard": "置顶到仪表盘", + "show_link_details": "显示链接详细信息", + "hide_link_details": "隐藏链接详细信息", + "link_pinned": "链接已置顶!", + "link_unpinned": "链接已取消置顶!", + "webpage": "网页", + "server_administration": "后台管理", + "all_collections": "全部收藏夹", + "dashboard": "仪表盘", + "demo_title": "仅限演示", + "demo_desc": "这只是 Linkwarden 的演示实例,禁止上传文件。", + "demo_desc_2": "如果你想尝试完整版,你可以注册免费试用:", + "demo_button": "以演示用户登录" +} \ No newline at end of file From 8758976f8d1d5b96ed8367c431fe8b5be782843d Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 15 Aug 2024 10:30:44 -0400 Subject: [PATCH 30/33] minor fix --- lib/api/controllers/links/postLink.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index c2105e3..852cdea 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -90,7 +90,7 @@ export default async function postLink( const newLink = await prisma.link.create({ data: { - url: link.url?.trim().replace(/\/+$/, "") || null, + url: link.url?.trim() || null, name, description: link.description, type: linkType, From 23860b8511d153e1b49140a141e01eedf7c2b9f1 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 15 Aug 2024 11:00:29 -0400 Subject: [PATCH 31/33] minor fix --- lib/api/generatePreview.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/api/generatePreview.ts b/lib/api/generatePreview.ts index 2693be2..2dc325b 100644 --- a/lib/api/generatePreview.ts +++ b/lib/api/generatePreview.ts @@ -24,7 +24,12 @@ const generatePreview = async ( 1024 * 1024 * Number(process.env.PREVIEW_MAX_BUFFER || 0.1) ) { console.log("Error generating preview: Buffer size exceeded"); - return; + return prisma.link.update({ + where: { id: linkId }, + data: { + preview: "unavailable", + }, + }); } await createFile({ From ca45076b6c1ce8beaa481846a1271208c399c4f6 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Thu, 15 Aug 2024 15:37:47 -0400 Subject: [PATCH 32/33] minor fix --- pages/links/index.tsx | 6 +++--- pages/links/pinned.tsx | 14 +++++++------- pages/search.tsx | 22 +--------------------- 3 files changed, 11 insertions(+), 31 deletions(-) diff --git a/pages/links/index.tsx b/pages/links/index.tsx index 582e962..fef2067 100644 --- a/pages/links/index.tsx +++ b/pages/links/index.tsx @@ -51,6 +51,9 @@ export default function Index() { /> + {!data.isLoading && links && !links[0] && ( + + )} - {!data.isLoading && links && !links[0] && ( - - )}
); diff --git a/pages/links/pinned.tsx b/pages/links/pinned.tsx index 9d1fb53..9eaf8e2 100644 --- a/pages/links/pinned.tsx +++ b/pages/links/pinned.tsx @@ -46,13 +46,6 @@ export default function PinnedLinks() { /> - {!data.isLoading && links && !links[0] && (
)} +
); diff --git a/pages/search.tsx b/pages/search.tsx index e628268..f8a1423 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -73,27 +73,7 @@ export default function Search() { - {/* { - !isLoading && - !links[0] ? ( -

{t("nothing_found")}

- ) : links[0] ? ( - - ) : ( - isLoading && ( - - ) - )} */} - + {!data.isLoading && links && !links[0] &&

{t("nothing_found")}

} Date: Thu, 15 Aug 2024 16:42:36 -0400 Subject: [PATCH 33/33] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2a357e..6a592e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkwarden", - "version": "v2.6.2", + "version": "v2.7.0", "main": "index.js", "repository": "https://github.com/linkwarden/linkwarden.git", "author": "Daniel31X13 ",