Compare commits
328 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90a3170ffd | |||
| b7656a3afc | |||
| 7109620dc7 | |||
| 834d25a99e | |||
| a67980b29d | |||
| 7880551c4d | |||
| 98fbb5b678 | |||
| 0c2c837028 | |||
| a5b166f41d | |||
| 89de1829c2 | |||
| fbca98984b | |||
| 06ab784441 | |||
| 4da2310e95 | |||
| a8f4072f1c | |||
| 93bcfc67fe | |||
| ba49946974 | |||
| d16b296b15 | |||
| 3fc61ac5ce | |||
| ced51e4801 | |||
| 254c090605 | |||
| 2a83ced9d8 | |||
| 52d333f085 | |||
| fbbb97b4cd | |||
| 4e29330472 | |||
| 44c82ff426 | |||
| 29e0370808 | |||
| 74399c1708 | |||
| 1dde8a6088 | |||
| e872c25332 | |||
| dea1e12700 | |||
| 055869883a | |||
| a5d3926d84 | |||
| eee6a807da | |||
| e24ae15a73 | |||
| f1dadf1546 | |||
| ee6dcdcc5b | |||
| e4cf682217 | |||
| e4fc8948fa | |||
| 9cfdb714c3 | |||
| ca093008b7 | |||
| 3dc99eff9d | |||
| 530e83ba34 | |||
| 4ae75634ca | |||
| 372ed248f1 | |||
| 8b3b7445c3 | |||
| 327bb10a08 | |||
| f9f2a8ca64 | |||
| 31c56b5009 | |||
| 7cf59bd430 | |||
| feb18b8c7a | |||
| ea9f940517 | |||
| 76a0151e43 | |||
| a78d9774a7 | |||
| dfbd56acc9 | |||
| 8a34413482 | |||
| 2e5f2deee7 | |||
| 320cddf224 | |||
| 86820c402b | |||
| e27fb90f14 | |||
| 848a33a53e | |||
| 98106b9f25 | |||
| 385bdc2343 | |||
| e9b47a69c5 | |||
| 1511ee1def | |||
| cb5e6de8b8 | |||
| b7387b1e08 | |||
| c0bca32462 | |||
| 79bf67f879 | |||
| 8529602252 | |||
| ad895eee17 | |||
| 31cf3c4f01 | |||
| 55c43d6f9e | |||
| b65787358f | |||
| 9e2f70d2eb | |||
| 71b99bb25c | |||
| 82452555e5 | |||
| ce746f33fd | |||
| 7131fde897 | |||
| a6e0af6b6e | |||
| f961ec0109 | |||
| 1677e132f3 | |||
| 141ca8f60b | |||
| fbc083c373 | |||
| feda50464c | |||
| 5e0f38c0d5 | |||
| e9acb548bf | |||
| e1f036adb2 | |||
| 1da960a3cf | |||
| 8c73ce60e9 | |||
| a481903b50 | |||
| 6632c0507b | |||
| fc679d1150 | |||
| d849b37f6c | |||
| dc6c17f8c4 | |||
| e2cf627ccd | |||
| de13a109c6 | |||
| 15c6213840 | |||
| 42f9dacffd | |||
| 4210913277 | |||
| 5f095b5631 | |||
| cf1306d2c4 | |||
| 500f7a338c | |||
| e910172558 | |||
| d906391ae2 | |||
| ae541bf2f5 | |||
| c28c73ce18 | |||
| f7d8ff9881 | |||
| c5b083e802 | |||
| 4d691e0cce | |||
| f5e7e373a8 | |||
| ef33f2c948 | |||
| d976761280 | |||
| 04ede17bfd | |||
| 9119402dac | |||
| d52afd66f3 | |||
| ae87b5698e | |||
| 1955cca589 | |||
| 8df0eab2a2 | |||
| e0bb7ffa08 | |||
| 7c35fe409f | |||
| ce9b4b05d4 | |||
| b246cdbc44 | |||
| a2b1513dbc | |||
| bcfbdf3e49 | |||
| f8ad08f5ed | |||
| 530ec69d1c | |||
| b74ff01ce6 | |||
| a001f70b9d | |||
| ca3eb29c48 | |||
| 9af695deaf | |||
| 650fa693bd | |||
| 099024518f | |||
| 6d227750c3 | |||
| 6ba2aab0ba | |||
| 375a55dd37 | |||
| 8e49ccf723 | |||
| ab83d1d0c6 | |||
| fc9de564b6 | |||
| 8786f8b5fe | |||
| a6a9402425 | |||
| b36dd49e16 | |||
| add781451a | |||
| e6979d4e75 | |||
| 9868ab61c9 | |||
| c367992116 | |||
| 64d361fa23 | |||
| 760c0b0026 | |||
| 3f4b7117bd | |||
| 361795ed47 | |||
| 93e4897c0b | |||
| 5b44bbcf59 | |||
| 8ba2cecf06 | |||
| 9cd165f2ce | |||
| ce5b1f444a | |||
| 4b1017f45b | |||
| 6f77882ffc | |||
| f8811a49c0 | |||
| 0e6b47d068 | |||
| a3106e072b | |||
| 1f20180a51 | |||
| ee05975e10 | |||
| 9c65e3e215 | |||
| ba72de19ef | |||
| ffc927759e | |||
| 33be9e5d83 | |||
| c447b36540 | |||
| 18e0b8b010 | |||
| 2042b94680 | |||
| 104c79cd99 | |||
| 1b1e4108ec | |||
| e253485e3d | |||
| e2a5f36008 | |||
| 17721c91b6 | |||
| 230110e912 | |||
| 4ac7110fb4 | |||
| e8a91bb551 | |||
| 9e4502c015 | |||
| a36769c521 | |||
| a3c6d9b42e | |||
| 2c9541734a | |||
| d40373032a | |||
| 732a5227d3 | |||
| 6d51b6de53 | |||
| 2fd21c8219 | |||
| cfc308f521 | |||
| 5850a423f9 | |||
| bef8ad976d | |||
| 64a1f352cf | |||
| 93e0fe6172 | |||
| 692b9b99e7 | |||
| 3b2b9e8279 | |||
| 82b743fa8d | |||
| 1ca6d72f82 | |||
| 916c69602d | |||
| b1dd9d66b6 | |||
| b51b08b0f4 | |||
| 0a398d1fd9 | |||
| d53dd93bb7 | |||
| 3c9d171f4d | |||
| af80614b3a | |||
| b88fa446be | |||
| a33d68c03a | |||
| ba7024db83 | |||
| 0f40578ca9 | |||
| 676c7c3a5d | |||
| 544585afd9 | |||
| 87196b1190 | |||
| 94d1bbbfba | |||
| cbd0ec6aa7 | |||
| f78eefbb3b | |||
| a8172a9dbe | |||
| 75d4fce8ec | |||
| 73954fe78e | |||
| 14f9378375 | |||
| 3afd5fef6e | |||
| b8b6fe24bc | |||
| 828e8eae2e | |||
| 1b53fb139d | |||
| c5d9f2c127 | |||
| f25b83bc09 | |||
| 9a15ca9684 | |||
| 557494747d | |||
| 5968bc6c9c | |||
| cf7b18e012 | |||
| 9ad277c784 | |||
| 9f181fb15e | |||
| 0c6911aaf0 | |||
| bd16136946 | |||
| 9eee3eea1d | |||
| 0579395e93 | |||
| 988d647521 | |||
| e628b3a6d5 | |||
| c73f13a9b0 | |||
| 9a28552af5 | |||
| 9938d21499 | |||
| eb78fb71d9 | |||
| e2fdb11a67 | |||
| e2f9439d40 | |||
| 30c9c86e22 | |||
| 614d92f050 | |||
| b50ec09727 | |||
| 01602bafec | |||
| d972ec2dab | |||
| 021f7c9481 | |||
| 59815f47d8 | |||
| b868318548 | |||
| 09ee81bf11 | |||
| 31663faa5a | |||
| 88a8c21aa4 | |||
| cd8081e610 | |||
| 1e0aaed833 | |||
| 34eec78ba4 | |||
| 11c834c61b | |||
| aefcd6d311 | |||
| dd09fd9026 | |||
| b19d6694ec | |||
| 49b1ea4875 | |||
| ea82fb5825 | |||
| e3b32fd791 | |||
| 3dfbccaf23 | |||
| 836dc10c2b | |||
| 946eed3773 | |||
| 359507c014 | |||
| 518b94b1f4 | |||
| f21033bd7a | |||
| fbc1d4b113 | |||
| dba62d7028 | |||
| 3aafc0960c | |||
| a2f03ff468 | |||
| c300da172b | |||
| 2f4af7f3d9 | |||
| cb5b1751c0 | |||
| 6f5245cbc4 | |||
| 9bee9b8ae4 | |||
| 7bdef522c1 | |||
| c8edc3844b | |||
| b5a28f68ad | |||
| ae1889e757 | |||
| b458fad567 | |||
| b1b0d98eb2 | |||
| b1c6a3faf1 | |||
| 56a281ae3d | |||
| ccafc997fc | |||
| 417c16d08b | |||
| dbeefecec6 | |||
| 35665ce292 | |||
| fb61812356 | |||
| ed91c4267b | |||
| c9c62b615b | |||
| de20fb7bc1 | |||
| 16024f40be | |||
| 2856e23a4a | |||
| db47a2a142 | |||
| ac795cdbdc | |||
| 9b6038201c | |||
| 9486d699c9 | |||
| cdcfabec0b | |||
| f9eedadb9f | |||
| c08e7d4580 | |||
| ea86737835 | |||
| 788fc56caf | |||
| 966136dab6 | |||
| 4454e615b6 | |||
| 91748ac5d2 | |||
| 2be2a83c62 | |||
| c38c5b2cc5 | |||
| cb8c2d5f10 | |||
| 8fdc503f55 | |||
| 97d8c35d2a | |||
| 4ffbc4e2f6 | |||
| 4252b79586 | |||
| ee4554ae95 | |||
| 697b139493 | |||
| a4ea023c51 | |||
| bcae97a296 | |||
| 565ee92d20 | |||
| ec4bfa6ba9 | |||
| 68f0f03d0d | |||
| 86cfdd508a | |||
| ed24685aaf | |||
| 84d4153b5c | |||
| ad525b8b00 | |||
| 24cced9dba | |||
| 3626ea613c | |||
| aaebdc5da7 | |||
| 748f181bc2 | |||
| d7705b585e | |||
| b3295e136d |
@@ -1,22 +1,22 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||
{
|
||||
"name": "Node.js & TypeScript",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
|
||||
"name": "Node.js & TypeScript",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "yarn install",
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "yarn install",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
"remoteUser": "root"
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
"remoteUser": "root"
|
||||
}
|
||||
|
||||
@@ -1,33 +1,381 @@
|
||||
NEXTAUTH_SECRET=very_sensitive_secret
|
||||
NEXTAUTH_URL=http://localhost:3000/api/v1/auth
|
||||
|
||||
# Manual installation database settings
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/linkwarden
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# Docker installation database settings
|
||||
POSTGRES_PASSWORD=super_secret_password
|
||||
|
||||
# Additional Optional Settings
|
||||
|
||||
PAGINATION_TAKE_COUNT=
|
||||
STORAGE_FOLDER=
|
||||
AUTOSCROLL_TIMEOUT=
|
||||
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
||||
NEXT_PUBLIC_CREDENTIALS_ENABLED=
|
||||
DISABLE_NEW_SSO_USERS=
|
||||
RE_ARCHIVE_LIMIT=
|
||||
NEXT_PUBLIC_MAX_FILE_SIZE=
|
||||
MAX_LINKS_PER_USER=
|
||||
ARCHIVE_TAKE_COUNT=
|
||||
BROWSER_TIMEOUT=
|
||||
IGNORE_UNAUTHORIZED_CA=
|
||||
|
||||
# AWS S3 Settings
|
||||
SPACES_KEY=
|
||||
SPACES_SECRET=
|
||||
SPACES_ENDPOINT=
|
||||
SPACES_BUCKET_NAME=
|
||||
SPACES_REGION=
|
||||
SPACES_FORCE_PATH_STYLE=
|
||||
|
||||
# SMTP Settings
|
||||
NEXT_PUBLIC_EMAIL_PROVIDER=
|
||||
EMAIL_FROM=
|
||||
EMAIL_SERVER=
|
||||
|
||||
# Stripe settings (You don't need these, it's for the cloud instance payments)
|
||||
NEXT_PUBLIC_STRIPE_IS_ACTIVE=
|
||||
STRIPE_SECRET_KEY=
|
||||
MONTHLY_PRICE_ID=
|
||||
YEARLY_PRICE_ID=
|
||||
NEXT_PUBLIC_TRIAL_PERIOD_DAYS=
|
||||
NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL=
|
||||
BASE_URL=http://localhost:3000
|
||||
|
||||
# Docker postgres settings
|
||||
POSTGRES_PASSWORD=
|
||||
#
|
||||
# SSO Providers
|
||||
#
|
||||
|
||||
# 42 School
|
||||
NEXT_PUBLIC_FORTYTWO_ENABLED=
|
||||
FORTYTWO_CUSTOM_NAME=
|
||||
FORTYTWO_CLIENT_ID=
|
||||
FORTYTWO_CLIENT_SECRET=
|
||||
|
||||
# Apple
|
||||
NEXT_PUBLIC_APPLE_ENABLED=
|
||||
APPLE_CUSTOM_NAME=
|
||||
APPLE_ID=
|
||||
APPLE_SECRET=
|
||||
|
||||
# Atlassian
|
||||
NEXT_PUBLIC_ATLASSIAN_ENABLED=
|
||||
ATLASSIAN_CUSTOM_NAME=
|
||||
ATLASSIAN_CLIENT_ID=
|
||||
ATLASSIAN_CLIENT_SECRET=
|
||||
ATLASSIAN_SCOPE=
|
||||
|
||||
# Auth0
|
||||
NEXT_PUBLIC_AUTH0_ENABLED=
|
||||
AUTH0_CUSTOM_NAME=
|
||||
AUTH0_ISSUER=
|
||||
AUTH0_CLIENT_SECRET=
|
||||
AUTH0_CLIENT_ID=
|
||||
|
||||
# Authentik
|
||||
NEXT_PUBLIC_AUTHENTIK_ENABLED=
|
||||
AUTHENTIK_CUSTOM_NAME=
|
||||
AUTHENTIK_ISSUER=
|
||||
AUTHENTIK_CLIENT_ID=
|
||||
AUTHENTIK_CLIENT_SECRET=
|
||||
|
||||
# Battle.net
|
||||
NEXT_PUBLIC_BATTLENET_ENABLED=
|
||||
BATTLENET_CUSTOM_NAME=
|
||||
BATTLENET_CLIENT_ID=
|
||||
BATTLENET_CLIENT_SECRET=
|
||||
BATLLENET_ISSUER=
|
||||
|
||||
# Box
|
||||
NEXT_PUBLIC_BOX_ENABLED=
|
||||
BOX_CUSTOM_NAME=
|
||||
BOX_CLIENT_ID=
|
||||
BOX_CLIENT_SECRET=
|
||||
|
||||
# Bungie
|
||||
NEXT_PUBLIC_BUNGIE_ENABLED=
|
||||
BUNGIE_CUSTOM_NAME=
|
||||
BUNGIE_CLIENT_ID=
|
||||
BUNGIE_CLIENT_SECRET=
|
||||
BUNGIE_API_KEY=
|
||||
|
||||
# Cognito
|
||||
NEXT_PUBLIC_COGNITO_ENABLED=
|
||||
COGNITO_CUSTOM_NAME=
|
||||
COGNITO_CLIENT_ID=
|
||||
COGNITO_CLIENT_SECRET=
|
||||
COGNITO_ISSUER=
|
||||
|
||||
# Coinbase
|
||||
NEXT_PUBLIC_COINBASE_ENABLED=
|
||||
COINBASE_CUSTOM_NAME=
|
||||
COINBASE_CLIENT_ID=
|
||||
COINBASE_CLIENT_SECRET=
|
||||
|
||||
# Discord
|
||||
NEXT_PUBLIC_DISCORD_ENABLED=
|
||||
DISCORD_CUSTOM_NAME=
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
|
||||
# Dropbox
|
||||
NEXT_PUBLIC_DROPBOX_ENABLED=
|
||||
DROPBOX_CUSTOM_NAME=
|
||||
DROPBOX_CLIENT_ID=
|
||||
DROPBOX_CLIENT_SECRET=
|
||||
|
||||
# DuendeIndentityServer6
|
||||
NEXT_PUBLIC_DUENDE_IDS6_ENABLED=
|
||||
DUENDE_IDS6_CUSTOM_NAME=
|
||||
DUENDE_IDS6_CLIENT_ID=
|
||||
DUENDE_IDS6_CLIENT_SECRET=
|
||||
DUENDE_IDS6_ISSUER=
|
||||
|
||||
# EVE Online
|
||||
NEXT_PUBLIC_EVEONLINE_ENABLED=
|
||||
EVEONLINE_CUSTOM_NAME=
|
||||
EVEONLINE_CLIENT_ID=
|
||||
EVEONLINE_CLIENT_SECRET=
|
||||
|
||||
# Facebook
|
||||
NEXT_PUBLIC_FACEBOOK_ENABLED=
|
||||
FACEBOOK_CUSTOM_NAME=
|
||||
FACEBOOK_CLIENT_ID=
|
||||
FACEBOOK_CLIENT_SECRET=
|
||||
|
||||
# FACEIT
|
||||
NEXT_PUBLIC_FACEIT_ENABLED=
|
||||
FACEIT_CUSTOM_NAME=
|
||||
FACEIT_CLIENT_ID=
|
||||
FACEIT_CLIENT_SECRET=
|
||||
|
||||
# Foursquare
|
||||
NEXT_PUBLIC_FOURSQUARE_ENABLED=
|
||||
FOURSQUARE_CUSTOM_NAME=
|
||||
FOURSQUARE_CLIENT_ID=
|
||||
FOURSQUARE_CLIENT_SECRET=
|
||||
FOURSQUARE_APIVERSION=
|
||||
|
||||
# Freshbooks
|
||||
NEXT_PUBLIC_FRESHBOOKS_ENABLED=
|
||||
FRESHBOOKS_CUSTOM_NAME=
|
||||
FRESHBOOKS_CLIENT_ID=
|
||||
FRESHBOOKS_CLIENT_SECRET=
|
||||
|
||||
# FusionAuth
|
||||
NEXT_PUBLIC_FUSIONAUTH_ENABLED=
|
||||
FUSIONAUTH_CUSTOM_NAME=
|
||||
FUSIONAUTH_CLIENT_ID=
|
||||
FUSIONAUTH_CLIENT_SECRET=
|
||||
FUSIONAUTH_ISSUER=
|
||||
FUSIONAUTH_TENANT_ID=
|
||||
|
||||
# GitHub
|
||||
NEXT_PUBLIC_GITHUB_ENABLED=
|
||||
GITHUB_CUSTOM_NAME=
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
|
||||
# GitLab
|
||||
NEXT_PUBLIC_GITLAB_ENABLED=
|
||||
GITLAB_CUSTOM_NAME=
|
||||
GITLAB_CLIENT_ID=
|
||||
GITLAB_CLIENT_SECRET=
|
||||
|
||||
# Google
|
||||
NEXT_PUBLIC_GOOGLE_ENABLED=
|
||||
GOOGLE_CUSTOM_NAME=
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# HubSpot
|
||||
NEXT_PUBLIC_HUBSPOT_ENABLED=
|
||||
HUBSPOT_CUSTOM_NAME=
|
||||
HUBSPOT_CLIENT_ID=
|
||||
HUBSPOT_CLIENT_SECRET=
|
||||
|
||||
# IdentityServer4
|
||||
NEXT_PUBLIC_IDS4_ENABLED=
|
||||
IDS4_CUSTOM_NAME=
|
||||
IDS4_CLIENT_ID=
|
||||
IDS4_CLIENT_SECRET=
|
||||
IDS4_ISSUER=
|
||||
|
||||
# Kakao
|
||||
NEXT_PUBLIC_KAKAO_ENABLED=
|
||||
KAKAO_CUSTOM_NAME=
|
||||
KAKAO_CLIENT_ID=
|
||||
KAKAO_CLIENT_SECRET=
|
||||
|
||||
# Keycloak
|
||||
NEXT_PUBLIC_KEYCLOAK_ENABLED=
|
||||
KEYCLOAK_CUSTOM_NAME=
|
||||
KEYCLOAK_ISSUER=
|
||||
KEYCLOAK_CLIENT_ID=
|
||||
KEYCLOAK_CLIENT_SECRET=
|
||||
|
||||
# LINE
|
||||
NEXT_PUBLIC_LINE_ENABLED=
|
||||
LINE_CUSTOM_NAME=
|
||||
LINE_CLIENT_ID=
|
||||
LINE_CLIENT_SECRET=
|
||||
|
||||
# LinkedIn
|
||||
NEXT_PUBLIC_LINKEDIN_ENABLED=
|
||||
LINKEDIN_CUSTOM_NAME=
|
||||
LINKEDIN_CLIENT_ID=
|
||||
LINKEDIN_CLIENT_SECRET=
|
||||
|
||||
# Mailchimp
|
||||
NEXT_PUBLIC_MAILCHIMP_ENABLED=
|
||||
MAILCHIMP_CUSTOM_NAME=
|
||||
MAILCHIMP_CLIENT_ID=
|
||||
MAILCHIMP_CLIENT_SECRET=
|
||||
|
||||
# Mail.ru
|
||||
NEXT_PUBLIC_MAILRU_ENABLED=
|
||||
MAILRU_CUSTOM_NAME=
|
||||
MAILRU_CLIENT_ID=
|
||||
MAILRU_CLIENT_SECRET=
|
||||
|
||||
# Naver
|
||||
NEXT_PUBLIC_NAVER_ENABLED=
|
||||
NAVER_CUSTOM_NAME=
|
||||
NAVER_CLIENT_ID=
|
||||
NAVER_CLIENT_SECRET=
|
||||
|
||||
# Netlify
|
||||
NEXT_PUBLIC_NETLIFY_ENABLED=
|
||||
NETLIFY_CUSTOM_NAME=
|
||||
NETLIFY_CLIENT_ID=
|
||||
NETLIFY_CLIENT_SECRET=
|
||||
|
||||
# Okta
|
||||
NEXT_PUBLIC_OKTA_ENABLED=
|
||||
OKTA_CUSTOM_NAME=
|
||||
OKTA_CLIENT_ID=
|
||||
OKTA_CLIENT_SECRET=
|
||||
OKTA_ISSUER=
|
||||
|
||||
# OneLogin
|
||||
NEXT_PUBLIC_ONELOGIN_ENABLED=
|
||||
ONELOGIN_CUSTOM_NAME=
|
||||
ONELOGIN_CLIENT_ID=
|
||||
ONELOGIN_CLIENT_SECRET=
|
||||
ONELOGIN_ISSUER=
|
||||
|
||||
# Osso
|
||||
NEXT_PUBLIC_OSSO_ENABLED=
|
||||
OSSO_CUSTOM_NAME=
|
||||
OSSO_CLIENT_ID=
|
||||
OSSO_CLIENT_SECRET=
|
||||
OSSO_ISSUER=
|
||||
|
||||
# osu!
|
||||
NEXT_PUBLIC_OSU_ENABLED=
|
||||
OSU_CUSTOM_NAME=
|
||||
OSU_CLIENT_ID=
|
||||
OSU_CLIENT_SECRET=
|
||||
|
||||
# Patreon
|
||||
NEXT_PUBLIC_PATREON_ENABLED=
|
||||
PATREON_CUSTOM_NAME=
|
||||
PATREON_CLIENT_ID=
|
||||
PATREON_CLIENT_SECRET=
|
||||
|
||||
# Pinterest
|
||||
NEXT_PUBLIC_PINTEREST_ENABLED=
|
||||
PINTEREST_CUSTOM_NAME=
|
||||
PINTEREST_CLIENT_ID=
|
||||
PINTEREST_CLIENT_SECRET=
|
||||
|
||||
# Pipedrive
|
||||
NEXT_PUBLIC_PIPEDRIVE_ENABLED=
|
||||
PIPEDRIVE_CUSTOM_NAME=
|
||||
PIPEDRIVE_CLIENT_ID=
|
||||
PIPEDRIVE_CLIENT_SECRET=
|
||||
|
||||
# Reddit
|
||||
NEXT_PUBLIC_REDDIT_ENABLED=
|
||||
REDDIT_CUSTOM_NAME=
|
||||
REDDIT_CLIENT_ID=
|
||||
REDDIT_CLIENT_SECRET=
|
||||
|
||||
# Salesforce
|
||||
NEXT_PUBLIC_SALESFORCE_ENABLED=
|
||||
SALESFORCE_CUSTOM_NAME=
|
||||
SALESFORCE_CLIENT_ID=
|
||||
SALESFORCE_CLIENT_SECRET=
|
||||
|
||||
# Slack
|
||||
NEXT_PUBLIC_SLACK_ENABLED=
|
||||
SLACK_CUSTOM_NAME=
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET=
|
||||
|
||||
# Spotify
|
||||
NEXT_PUBLIC_SPOTIFY_ENABLED=
|
||||
SPOTIFY_CUSTOM_NAME=
|
||||
SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_CLIENT_SECRET=
|
||||
|
||||
# Strava
|
||||
NEXT_PUBLIC_STRAVA_ENABLED=
|
||||
STRAVA_CUSTOM_NAME=
|
||||
STRAVA_CLIENT_ID=
|
||||
STRAVA_CLIENT_SECRET=
|
||||
|
||||
# Todoist
|
||||
NEXT_PUBLIC_TODOIST_ENABLED=
|
||||
TODOIST_CUSTOM_NAME=
|
||||
TODOIST_CLIENT_ID=
|
||||
TODOIST_CLIENT_SECRET=
|
||||
|
||||
# Twitch
|
||||
NEXT_PUBLIC_TWITCH_ENABLED=
|
||||
TWITCH_CUSTOM_NAME=
|
||||
TWITCH_CLIENT_ID=
|
||||
TWITCH_CLIENT_SECRET=
|
||||
|
||||
# United Effects
|
||||
NEXT_PUBLIC_UNITED_EFFECTS_ENABLED=
|
||||
UNITED_EFFECTS_CUSTOM_NAME=
|
||||
UNITED_EFFECTS_CLIENT_ID=
|
||||
UNITED_EFFECTS_CLIENT_SECRET=
|
||||
UNITED_EFFECTS_ISSUER=
|
||||
|
||||
# VK
|
||||
NEXT_PUBLIC_VK_ENABLED=
|
||||
VK_CUSTOM_NAME=
|
||||
VK_CLIENT_ID=
|
||||
VK_CLIENT_SECRET=
|
||||
|
||||
# Wikimedia
|
||||
NEXT_PUBLIC_WIKIMEDIA_ENABLED=
|
||||
WIKIMEDIA_CUSTOM_NAME=
|
||||
WIKIMEDIA_CLIENT_ID=
|
||||
WIKIMEDIA_CLIENT_SECRET=
|
||||
|
||||
# Wordpress.com
|
||||
NEXT_PUBLIC_WORDPRESS_ENABLED=
|
||||
WORDPRESS_CUSTOM_NAME=
|
||||
WORDPRESS_CLIENT_ID=
|
||||
WORDPRESS_CLIENT_SECRET=
|
||||
|
||||
# Yandex
|
||||
NEXT_PUBLIC_YANDEX_ENABLED=
|
||||
YANDEX_CUSTOM_NAME=
|
||||
YANDEX_CLIENT_ID=
|
||||
YANDEX_CLIENT_SECRET=
|
||||
|
||||
# Zitadel
|
||||
NEXT_PUBLIC_ZITADEL_ENABLED=
|
||||
ZITADEL_CUSTOM_NAME=
|
||||
ZITADEL_CLIENT_ID=
|
||||
ZITADEL_CLIENT_SECRET=
|
||||
ZITADEL_ISSUER=
|
||||
|
||||
# Zoho
|
||||
NEXT_PUBLIC_ZOHO_ENABLED=
|
||||
ZOHO_CUSTOM_NAME=
|
||||
ZOHO_CLIENT_ID=
|
||||
ZOHO_CLIENT_SECRET=
|
||||
|
||||
# Zoom
|
||||
NEXT_PUBLIC_ZOOM_ENABLED=
|
||||
ZOOM_CUSTOM_NAME=
|
||||
ZOOM_CLIENT_ID=
|
||||
ZOOM_CLIENT_SECRET=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"react-hooks/exhaustive-deps": "off"
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@next/next/no-img-element": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Ask a Question
|
||||
about: Ask about a particular topic
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your question related to a problem or code? Please describe.**
|
||||
A clear and concise description of what the problem or code is. Ex. I'm confused about how [...] works, or I'm facing an issue when [...]
|
||||
|
||||
**Describe what you've tried to solve this question**
|
||||
Explain what steps or research you've already taken to try and understand or solve this.
|
||||
|
||||
**Include any code or screenshots (if applicable)**
|
||||
Add any code snippets, error messages, or screenshots that might help others understand your question better.
|
||||
|
||||
**Additional context**
|
||||
Include any additional context or details that might help get a clearer understanding of your question.
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Bug report
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Feature request
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -1,17 +1,19 @@
|
||||
# Security Policy
|
||||
# Security
|
||||
|
||||
## Supported Versions
|
||||
The Linkwarden team and community take security bugs in Linkwarden seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| 1.x.x | ✅ |
|
||||
# Reporting Security Issues
|
||||
|
||||
## Reporting a Vulnerability
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
First off, we really appreciate the time you spent!
|
||||
Instead, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/linkwarden/linkwarden/security/advisories/new) tab.
|
||||
|
||||
If you found a vulnerability, these are the ways you can reach us:
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message:
|
||||
[security@linkwarden.app](mailto:security@linkwarden.app)
|
||||
|
||||
Email: [security@linkwarden.app](mailto:security@linkwarden.app)
|
||||
|
||||
Or you can directly DM me via Twitter: [@daniel31x13](https://twitter.com/Daniel31X13).
|
||||
After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
||||
|
||||
# Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.next
|
||||
public
|
||||
|
||||
*.lock
|
||||
*.log
|
||||
|
||||
.github
|
||||
|
||||
data
|
||||
pgdata
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,5 +1,4 @@
|
||||
# playwright doesnt support debian image
|
||||
FROM node:20-bullseye-slim
|
||||
FROM node:18.18-bullseye-slim
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -9,8 +8,10 @@ WORKDIR /data
|
||||
|
||||
COPY ./package.json ./yarn.lock ./playwright.config.ts ./
|
||||
|
||||
RUN yarn && \
|
||||
npx playwright install-deps && \
|
||||
# Increase timeout to pass github actions arm64 build
|
||||
RUN --mount=type=cache,sharing=locked,target=/usr/local/share/.cache/yarn yarn install --network-timeout 10000000
|
||||
|
||||
RUN npx playwright install-deps && \
|
||||
apt-get clean && \
|
||||
yarn cache clean
|
||||
|
||||
|
||||
@@ -2,57 +2,93 @@
|
||||
<img src="./assets/logo.png" width="100px" />
|
||||
<h1>Linkwarden</h1>
|
||||
|
||||
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat-square" alt="Discord"></a>
|
||||
<img alt="GitHub commits since latest release (by SemVer including pre-releases)" src="https://img.shields.io/github/commits-since/linkwarden/linkwarden/v1.1.0/dev">
|
||||
<img src="https://img.shields.io/github/languages/top/linkwarden/linkwarden?style=flat-square" alt="Top Language">
|
||||
<img src="https://img.shields.io/github/stars/linkwarden/linkwarden?style=flat-square" alt="Github Stars">
|
||||
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat" alt="Discord"></a>
|
||||
<a href="https://twitter.com/LinkwardenHQ"><img src="https://img.shields.io/twitter/follow/linkwarden" alt="Twitter"></a>
|
||||
|
||||
<img alt="GitHub commits since latest release" src="https://img.shields.io/github/commits-since/linkwarden/linkwarden/latest/dev?style=for-the-badge&label=COMMITS%20SINCE%20LATEST%20RELEASE">
|
||||
|
||||
</div>
|
||||
|
||||
<div align='center'>
|
||||
|
||||
[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) | [Screenshots](https://github.com/linkwarden/linkwarden#screenshots) | [Support ❤](https://github.com/linkwarden/linkwarden#support-)
|
||||
[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-)
|
||||
|
||||
</div>
|
||||
|
||||
## Intro & motivation
|
||||
|
||||
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.** The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
|
||||
**Linkwarden is a self-hosted, open-source collaborative bookmark manager to collect, organize and archive webpages.**
|
||||
|
||||
The objective is to organize useful webpages and articles you find across the web in one place, and since useful webpages can go away (see the inevitability of [Link Rot](https://www.howtogeek.com/786227/what-is-link-rot-and-how-does-it-threaten-the-web/)), Linkwarden also saves a copy of each webpage as a Screenshot and PDF, ensuring accessibility even if the original content is no longer available.
|
||||
|
||||
Additionally, Linkwarden is designed with collaboration in mind, sharing links with the public and/or allowing multiple users to work together seamlessly.
|
||||
|
||||
<img src="./assets/showcase_image.png" />
|
||||
|
||||
> **Note**
|
||||
> [!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. <br> Your subscription supports our hosting infrastructure and ongoing development. <br> 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.
|
||||
|
||||
<img src="./assets/dashboard.png" />
|
||||
|
||||
<div align="center">
|
||||
<img src="./assets/all_links.jpg" width="23%" />
|
||||
|
||||
<img src="./assets/list_view.jpg" width="23%" />
|
||||
|
||||
<img src="./assets/all_collections.jpg" width="23%" />
|
||||
|
||||
<img src="./assets/manage_team.jpg" width="23%" />
|
||||
|
||||
<img src="./assets/readable_view.jpg" width="23%" />
|
||||
|
||||
<img src="./assets/preserved_formats.jpg" width="23%" />
|
||||
|
||||
<img src="./assets/public_page.jpg" width="23%" />
|
||||
|
||||
<img src="./assets/light_dashboard.jpg" width="23%" />
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary><b>A bit of a "history"</b></summary>
|
||||
Linkwarden has been completely rebuilt and redesigned from ground up, so pretty much the only thing it has in common with its predecessor is the idea behind it - bookmark management.
|
||||
|
||||
**What happened to the old version?**
|
||||
We highly recommend that you don't use the old version because it is no longer maintained and has far fewer features. However, if you still want to check it out, we've forked the old version from the current repository into [this repo](https://github.com/linkwarden/linkwarden-old).
|
||||
We've forked the old version from the current repository into [this repo](https://github.com/linkwarden/linkwarden-old).
|
||||
|
||||
</details>
|
||||
|
||||
## Features
|
||||
|
||||
- 📸 Auto capture a screenshot and a PDF of each link.
|
||||
- 🏛️ Send your webpage to Wayback Machine archive.org for a snapshot.
|
||||
- 📸 Auto capture a screenshot, PDF, and readable view of each webpage.
|
||||
- 🏛️ Send your webpage to Wayback Machine ([archive.org](https://archive.org)) for a snapshot. (Optional)
|
||||
- 📂 Organize links by collection, name, description and multiple tags.
|
||||
- 👥 Collaborate on gathering links in a collection.
|
||||
- 🔐 Customize the permissions of each member.
|
||||
- 🌐 Share your collected links with the world.
|
||||
- 🎛️ Customize the permissions of each member.
|
||||
- 🌐 Share your collected links and preserved formats with the world.
|
||||
- 📌 Pin your favorite links to dashboard.
|
||||
- 🔍 Search, filter and sort by link details.
|
||||
- 📱 Responsive design and supports most browsers.
|
||||
- 🔍 Full text search, filter and sort for easy retrieval.
|
||||
- 📱 Responsive design and supports most modern browsers.
|
||||
- 🌓 Dark/Light mode support.
|
||||
- 🧩 Browser extension, managed by the community [check it out!](https://github.com/linkwarden/browser-extension)
|
||||
- ⬇️ Import your bookmarks from other browsers.
|
||||
- 🧩 Browser extension, managed by the community. [Star it here!](https://github.com/linkwarden/browser-extension)
|
||||
- ⬇️ Import and export your bookmarks.
|
||||
- 🔐 SSO integration. (Enterprise and Self-hosted users only)
|
||||
- ✨ And so many more features!
|
||||
|
||||
## Like what we're doing? Give us a Star ⭐
|
||||
|
||||

|
||||
|
||||
## We're building our Community 🌐
|
||||
|
||||
Join and follow us in the following platforms to stay up to date about the most recent features and for support:
|
||||
|
||||
<a href="https://discord.com/invite/CtuYV47nuJ"><img src="https://img.shields.io/discord/1117993124669702164?logo=discord&style=flat" alt="Discord"></a>
|
||||
|
||||
<a href="https://twitter.com/LinkwardenHQ"><img src="https://img.shields.io/twitter/follow/linkwarden" alt="Twitter"></a>
|
||||
|
||||
<a href="https://fosstodon.org/@linkwarden"><img src="https://img.shields.io/mastodon/follow/110748840237143200?domain=https%3A%2F%2Ffosstodon.org" alt="Mastodon"></a>
|
||||
|
||||
## Suggestions
|
||||
|
||||
We usually go after the [popular suggestions](https://github.com/linkwarden/linkwarden/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc). Feel free to open a [new issue](https://github.com/linkwarden/linkwarden/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md&title=) to suggest one - others might be interested too! :)
|
||||
We _usually_ go after the [popular suggestions](https://github.com/linkwarden/linkwarden/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc). Feel free to open a [new issue](https://github.com/linkwarden/linkwarden/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md&title=) to suggest one - others might be interested too! :)
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -62,32 +98,14 @@ Make sure to check out our [public roadmap](https://github.com/orgs/linkwarden/p
|
||||
|
||||
For information on how to get started or to set up your own instance, please visit the [documentation](https://docs.linkwarden.app).
|
||||
|
||||
## Main Tech Stack
|
||||
|
||||
- NextJS
|
||||
- TypeScript
|
||||
- Tailwind
|
||||
- Prisma
|
||||
- Zustand
|
||||
|
||||
## Development
|
||||
|
||||
If you want to contribute, Thanks! Start by checking our [public roadmap](https://github.com/orgs/linkwarden/projects/1), there you'll see a [README for contributers](https://github.com/orgs/linkwarden/projects/1?pane=issue&itemId=34708277) for the rest of the info on how to contribute to this repo.
|
||||
If you want to contribute, Thanks! Start by checking our [public roadmap](https://github.com/orgs/linkwarden/projects/1), there you'll see a [README for contributers](https://github.com/orgs/linkwarden/projects/1?pane=issue&itemId=34708277) for the rest of the info on how to contribute and the main tech stack.
|
||||
|
||||
## Security
|
||||
|
||||
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!
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div align="center">
|
||||
<img src="./assets/collections.png" height="150" />
|
||||
|
||||
<img src="./assets/collaborators.png" height="150" />
|
||||
|
||||
<img src="./assets/link_details.png" height="150" />
|
||||
</div>
|
||||
|
||||
## Support ❤
|
||||
|
||||
Other than using our official [Cloud](https://linkwarden.app/#pricing) offering, any [donations](https://opencollective.com/linkwarden) are highly appreciated as well!
|
||||
@@ -96,7 +114,12 @@ Here are the other ways to support/cheer this project:
|
||||
|
||||
- Starring this repository.
|
||||
- Joining us on [Discord](https://discord.com/invite/CtuYV47nuJ).
|
||||
- Following @daniel31x13 on [Mastodon](https://mastodon.social/@daniel31x13), [Twitter](https://twitter.com/daniel31x13) and [GitHub](https://github.com/daniel31x13).
|
||||
- Referring Linkwarden to a friend.
|
||||
|
||||
If you did any of the above, Thanksss! Otherwise thanks.
|
||||
|
||||
## Thanks to All the Contributors 💪
|
||||
|
||||
Huge thanks to these guys for spending their time helping Linkwarden grow. They rock! ⚡️
|
||||
|
||||
<img src="https://contributors-img.web.app/image?repo=linkwarden/linkwarden" alt="Contributors"/>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||
|
||||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||
# release/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/gradle.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/dictionaries
|
||||
.idea/libraries
|
||||
# Android Studio 3 in .gitignore file.
|
||||
.idea/caches
|
||||
.idea/modules.xml
|
||||
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
freeline/
|
||||
freeline_project_description.json
|
||||
|
||||
# fastlane
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
fastlane/readme.md
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# lint
|
||||
lint/intermediates/
|
||||
lint/generated/
|
||||
lint/outputs/
|
||||
lint/tmp/
|
||||
# lint/reports/
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Cordova plugins for Capacitor
|
||||
capacitor-cordova-android-plugins
|
||||
|
||||
# Copied web assets
|
||||
app/src/main/assets/public
|
||||
|
||||
# Generated Config files
|
||||
app/src/main/assets/capacitor.config.json
|
||||
app/src/main/assets/capacitor.plugins.json
|
||||
app/src/main/res/xml/config.xml
|
||||
@@ -0,0 +1,2 @@
|
||||
/build/*
|
||||
!/build/.npmkeep
|
||||
@@ -0,0 +1,54 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
namespace "com.example.app"
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.example.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
flatDir{
|
||||
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation project(':capacitor-cordova-android-plugins')
|
||||
}
|
||||
|
||||
apply from: 'capacitor.build.gradle'
|
||||
|
||||
try {
|
||||
def servicesJSON = file('google-services.json')
|
||||
if (servicesJSON.text) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (hasProperty('postBuildExtras')) {
|
||||
postBuildExtras()
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
|
||||
@Test
|
||||
public void useAppContext() throws Exception {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.app;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#26A69A"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<WebView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">linkwarden</string>
|
||||
<string name="title_activity_main">linkwarden</string>
|
||||
<string name="package_name">com.example.app</string>
|
||||
<string name="custom_url_scheme">com.example.app</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="my_images" path="." />
|
||||
<cache-path name="my_cache_images" path="." />
|
||||
</paths>
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.getcapacitor.myapp;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.0.0'
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "variables.gradle"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
@@ -0,0 +1,22 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
@@ -0,0 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,5 @@
|
||||
include ':app'
|
||||
include ':capacitor-cordova-android-plugins'
|
||||
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||
|
||||
apply from: 'capacitor.settings.gradle'
|
||||
@@ -0,0 +1,16 @@
|
||||
ext {
|
||||
minSdkVersion = 22
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
androidxActivityVersion = '1.7.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.10.0'
|
||||
androidxFragmentVersion = '1.5.6'
|
||||
coreSplashScreenVersion = '1.0.0'
|
||||
androidxWebkitVersion = '1.6.1'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
}
|
||||
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
Before Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 474 KiB |
|
After Width: | Height: | Size: 786 KiB |
|
After Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 387 KiB |
|
After Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
@@ -0,0 +1,19 @@
|
||||
import { CapacitorConfig } from "@capacitor/cli";
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: "com.example.app",
|
||||
appName: "linkwarden",
|
||||
webDir: "out",
|
||||
server: {
|
||||
androidScheme: "https",
|
||||
url: "http://localhost:3000",
|
||||
cleartext: true,
|
||||
},
|
||||
plugins: {
|
||||
CapacitorHttp: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,29 @@
|
||||
type Props = {
|
||||
onClick?: Function;
|
||||
label: string;
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
type?: "button" | "submit" | "reset" | undefined;
|
||||
};
|
||||
|
||||
export default function AccentSubmitButton({
|
||||
onClick,
|
||||
label,
|
||||
loading,
|
||||
className,
|
||||
type,
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
type={type ? type : undefined}
|
||||
className={`border primary-btn-gradient select-none duration-200 bg-black border-[oklch(var(--p))] hover:border-[#0070b5] rounded-lg text-center px-4 py-2 text-white active:scale-95 tracking-wider w-fit flex justify-center items-center gap-2 ${
|
||||
className || ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (loading !== undefined && !loading && onClick) onClick();
|
||||
}}
|
||||
>
|
||||
<p className="font-bold">{label}</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import Link from "next/link";
|
||||
import React, { MouseEventHandler } from "react";
|
||||
|
||||
type Props = {
|
||||
toggleAnnouncementBar: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
|
||||
return (
|
||||
<div className="fixed w-full z-20 bg-base-200">
|
||||
<div className="w-full h-10 rainbow flex items-center justify-center">
|
||||
<div className="w-fit font-semibold">
|
||||
🎉️ See what's new in{" "}
|
||||
<Link
|
||||
href="https://blog.linkwarden.app/releases/v2.4"
|
||||
target="_blank"
|
||||
className="underline hover:opacity-50 duration-100"
|
||||
>
|
||||
Linkwarden v2.4
|
||||
</Link>
|
||||
! 🥳️
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="fixed right-3 hover:opacity-50 duration-100"
|
||||
onClick={toggleAnnouncementBar}
|
||||
>
|
||||
<i className="bi-x text-neutral text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { faSquare, faSquareCheck } from "@fortawesome/free-regular-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { ChangeEventHandler } from "react";
|
||||
|
||||
type Props = {
|
||||
@@ -11,24 +9,18 @@ type Props = {
|
||||
|
||||
export default function Checkbox({ label, state, className, onClick }: Props) {
|
||||
return (
|
||||
<label className={`cursor-pointer flex items-center gap-2 ${className}`}>
|
||||
<label
|
||||
className={`label cursor-pointer flex gap-2 justify-start ${
|
||||
className || ""
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state}
|
||||
onChange={onClick}
|
||||
className="peer sr-only"
|
||||
className="checkbox checkbox-primary"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faSquareCheck}
|
||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:block hidden"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faSquare}
|
||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:hidden block"
|
||||
/>
|
||||
<span className="text-black dark:text-white rounded select-none">
|
||||
{label}
|
||||
</span>
|
||||
<span className="label-text">{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ type Props = {
|
||||
children: ReactNode;
|
||||
onClickOutside: Function;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onMount?: (rect: DOMRect) => void;
|
||||
};
|
||||
|
||||
function useOutsideAlerter(
|
||||
@@ -30,12 +32,22 @@ export default function ClickAwayHandler({
|
||||
children,
|
||||
onClickOutside,
|
||||
className,
|
||||
style,
|
||||
onMount,
|
||||
}: Props) {
|
||||
const wrapperRef = useRef(null);
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useOutsideAlerter(wrapperRef, onClickOutside);
|
||||
|
||||
useEffect(() => {
|
||||
if (wrapperRef.current && onMount) {
|
||||
const rect = wrapperRef.current.getBoundingClientRect();
|
||||
onMount(rect); // Pass the bounding rectangle to the parent
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={className}>
|
||||
<div ref={wrapperRef} className={className} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEllipsis, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import Link from "next/link";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import Dropdown from "./Dropdown";
|
||||
import { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ProfilePhoto from "./ProfilePhoto";
|
||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
||||
import useModalStore from "@/store/modals";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import { useTheme } from "next-themes";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
collection: CollectionIncludingMembersAndLinkCount;
|
||||
@@ -16,9 +16,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function CollectionCard({ collection, className }: Props) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const { theme } = useTheme();
|
||||
const { settings } = useLocalSettingsStore();
|
||||
const { account } = useAccountStore();
|
||||
|
||||
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
|
||||
"en-US",
|
||||
@@ -29,132 +28,189 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||
}
|
||||
);
|
||||
|
||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||
|
||||
const permissions = usePermissions(collection.id as number);
|
||||
|
||||
const [collectionOwner, setCollectionOwner] = useState({
|
||||
id: null as unknown as number,
|
||||
name: "",
|
||||
username: "",
|
||||
image: "",
|
||||
archiveAsScreenshot: undefined as unknown as boolean,
|
||||
archiveAsPDF: undefined as unknown as boolean,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOwner = async () => {
|
||||
if (collection && collection.ownerId !== account.id) {
|
||||
const owner = await getPublicUserData(collection.ownerId as number);
|
||||
setCollectionOwner(owner);
|
||||
} else if (collection && collection.ownerId === account.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,
|
||||
archiveAsPDF: account.archiveAsPDF as boolean,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchOwner();
|
||||
}, [collection]);
|
||||
|
||||
const [editCollectionModal, setEditCollectionModal] = useState(false);
|
||||
const [editCollectionSharingModal, setEditCollectionSharingModal] =
|
||||
useState(false);
|
||||
const [deleteCollectionModal, setDeleteCollectionModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
|
||||
theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 50%, ${theme === "dark" ? "#262626" : "#f9fafb"} 100%)`,
|
||||
}}
|
||||
className={`border border-solid border-sky-100 dark:border-neutral-700 self-stretch min-h-[12rem] rounded-2xl shadow duration-100 hover:shadow-none hover:opacity-80 group relative ${className}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="dropdown dropdown-bottom dropdown-end absolute top-3 right-3 z-20">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||
>
|
||||
<i className="bi-three-dots text-xl" title="More"></i>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
|
||||
{permissions === true ? (
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Edit Collection Info
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditCollectionSharingModal(true);
|
||||
}}
|
||||
>
|
||||
{permissions === true ? "Share and Collaborate" : "View Team"}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setDeleteCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
{permissions === true ? "Delete Collection" : "Leave Collection"}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setExpandDropdown(!expandDropdown)}
|
||||
id={"expand-dropdown" + collection.id}
|
||||
className="inline-flex absolute top-5 right-5 rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full"
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
id={"expand-dropdown" + collection.id}
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
{collectionOwner.id ? (
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
) : undefined}
|
||||
{collection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
name={e.user.name}
|
||||
className="-ml-3"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{collection.members.length - 3 > 0 ? (
|
||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{collection.members.length - 3}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Link
|
||||
href={`/collections/${collection.id}`}
|
||||
className="flex flex-col gap-2 justify-between min-h-[12rem] h-full select-none p-5"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
|
||||
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||
} 50%, ${
|
||||
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||
} 100%)`,
|
||||
}}
|
||||
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content"
|
||||
>
|
||||
<p className="text-2xl capitalize text-black dark:text-white break-words line-clamp-3 w-4/5">
|
||||
{collection.name}
|
||||
</p>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center w-full">
|
||||
{collection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={`/api/avatar/${e.userId}?${Date.now()}`}
|
||||
className="-mr-3 border-[3px]"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 4)}
|
||||
{collection.members.length - 4 > 0 ? (
|
||||
<div className="h-10 w-10 text-white flex items-center justify-center rounded-full border-[3px] bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700 -mr-3">
|
||||
+{collection.members.length - 4}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="card-body flex flex-col justify-between min-h-[12rem]">
|
||||
<div className="flex justify-between">
|
||||
<p className="card-title break-words line-clamp-2 w-full">
|
||||
{collection.name}
|
||||
</p>
|
||||
<div className="w-8 h-8 ml-10"></div>
|
||||
</div>
|
||||
<div className="text-right w-40">
|
||||
<div className="text-black dark:text-white font-bold text-sm flex justify-end gap-1 items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
{collection._count && collection._count.links}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1 text-gray-500 dark:text-gray-300">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||
<p className="font-bold text-xs">{formattedDate}</p>
|
||||
|
||||
<div className="flex justify-end items-center">
|
||||
<div className="text-right">
|
||||
<div className="font-bold text-sm flex justify-end gap-1 items-center">
|
||||
{collection.isPublic ? (
|
||||
<i
|
||||
className="bi-globe-americas drop-shadow text-neutral"
|
||||
title="This collection is being shared publicly."
|
||||
></i>
|
||||
) : undefined}
|
||||
<i
|
||||
className="bi-link-45deg text-lg text-neutral"
|
||||
title="This collection is being shared publicly."
|
||||
></i>
|
||||
{collection._count && collection._count.links}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1 text-neutral">
|
||||
<p className="font-bold text-xs flex gap-1 items-center">
|
||||
<i
|
||||
className="bi-calendar3 text-neutral"
|
||||
title="This collection is being shared publicly."
|
||||
></i>
|
||||
{formattedDate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
{expandDropdown ? (
|
||||
<Dropdown
|
||||
items={[
|
||||
permissions === true
|
||||
? {
|
||||
name: "Edit Collection Info",
|
||||
onClick: () => {
|
||||
collection &&
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwner: permissions === true,
|
||||
active: collection,
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
name: permissions === true ? "Share/Collaborate" : "View Team",
|
||||
onClick: () => {
|
||||
collection &&
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwner: permissions === true,
|
||||
active: collection,
|
||||
defaultIndex: permissions === true ? 1 : 0,
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name:
|
||||
permissions === true ? "Delete Collection" : "Leave Collection",
|
||||
onClick: () => {
|
||||
collection &&
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwner: permissions === true,
|
||||
active: collection,
|
||||
defaultIndex: permissions === true ? 2 : 1,
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
},
|
||||
]}
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "expand-dropdown" + collection.id)
|
||||
setExpandDropdown(false);
|
||||
}}
|
||||
className="absolute top-[3.2rem] right-5 z-10"
|
||||
{editCollectionModal ? (
|
||||
<EditCollectionModal
|
||||
onClose={() => setEditCollectionModal(false)}
|
||||
activeCollection={collection}
|
||||
/>
|
||||
) : null}
|
||||
) : undefined}
|
||||
{editCollectionSharingModal ? (
|
||||
<EditCollectionSharingModal
|
||||
onClose={() => setEditCollectionSharingModal(false)}
|
||||
activeCollection={collection}
|
||||
/>
|
||||
) : undefined}
|
||||
{deleteCollectionModal ? (
|
||||
<DeleteCollectionModal
|
||||
onClose={() => setDeleteCollectionModal(false)}
|
||||
activeCollection={collection}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export default function dashboardItem({
|
||||
name,
|
||||
value,
|
||||
icon,
|
||||
}: {
|
||||
name: string;
|
||||
value: number;
|
||||
icon: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="w-[4.7rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
|
||||
<i className={`${icon} text-primary text-4xl drop-shadow`}></i>
|
||||
</div>
|
||||
<div className="ml-4 flex flex-col justify-center">
|
||||
<p className="text-neutral text-xs tracking-wider">{name}</p>
|
||||
<p className="font-thin text-6xl text-primary mt-0.5">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import React, { MouseEventHandler } from "react";
|
||||
import React, { MouseEventHandler, useEffect, useState } from "react";
|
||||
import ClickAwayHandler from "./ClickAwayHandler";
|
||||
|
||||
type MenuItem =
|
||||
@@ -19,19 +19,72 @@ type Props = {
|
||||
onClickOutside: Function;
|
||||
className?: string;
|
||||
items: MenuItem[];
|
||||
points?: { x: number; y: number };
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export default function Dropdown({ onClickOutside, className, items }: Props) {
|
||||
return (
|
||||
export default function Dropdown({
|
||||
points,
|
||||
onClickOutside,
|
||||
className,
|
||||
items,
|
||||
}: Props) {
|
||||
const [pos, setPos] = useState<{ x: number; y: number }>();
|
||||
const [dropdownHeight, setDropdownHeight] = useState<number>();
|
||||
const [dropdownWidth, setDropdownWidth] = useState<number>();
|
||||
|
||||
function convertRemToPixels(rem: number) {
|
||||
return (
|
||||
rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (points) {
|
||||
let finalX = points.x;
|
||||
let finalY = points.y;
|
||||
|
||||
// Check for x-axis overflow (left side)
|
||||
if (dropdownWidth && points.x + dropdownWidth > window.innerWidth) {
|
||||
finalX = points.x - dropdownWidth;
|
||||
}
|
||||
|
||||
// Check for y-axis overflow (bottom side)
|
||||
if (dropdownHeight && points.y + dropdownHeight > window.innerHeight) {
|
||||
finalY =
|
||||
window.innerHeight -
|
||||
(dropdownHeight + (window.innerHeight - points.y));
|
||||
}
|
||||
|
||||
setPos({ x: finalX, y: finalY });
|
||||
}
|
||||
}, [points, dropdownHeight]);
|
||||
|
||||
return !points || pos ? (
|
||||
<ClickAwayHandler
|
||||
onMount={(e) => {
|
||||
setDropdownHeight(e.height);
|
||||
setDropdownWidth(e.width);
|
||||
}}
|
||||
style={
|
||||
points
|
||||
? {
|
||||
position: "fixed",
|
||||
top: `${pos?.y}px`,
|
||||
left: `${pos?.x}px`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClickOutside={onClickOutside}
|
||||
className={`${className} py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
|
||||
className={`${
|
||||
className || ""
|
||||
} py-1 shadow-md border border-neutral-content bg-base-200 rounded-md flex flex-col z-20`}
|
||||
>
|
||||
{items.map((e, i) => {
|
||||
const inner = e && (
|
||||
<div className="cursor-pointer rounded-md">
|
||||
<div className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 dark:hover:bg-neutral-700 duration-100">
|
||||
<p className="text-black dark:text-white select-none">{e.name}</p>
|
||||
<div className="flex items-center gap-2 py-1 px-2 hover:bg-base-100 duration-100">
|
||||
<p className="select-none">{e.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -49,5 +102,5 @@ export default function Dropdown({ onClickOutside, className, items }: Props) {
|
||||
);
|
||||
})}
|
||||
</ClickAwayHandler>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,130 @@
|
||||
import React, { SetStateAction } from "react";
|
||||
import ClickAwayHandler from "./ClickAwayHandler";
|
||||
import Checkbox from "./Checkbox";
|
||||
import { LinkSearchFilter } from "@/types/global";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
setFilterDropdown: (value: SetStateAction<boolean>) => void;
|
||||
setSearchFilter: Function;
|
||||
searchFilter: LinkSearchFilter;
|
||||
searchFilter: {
|
||||
name: boolean;
|
||||
url: boolean;
|
||||
description: boolean;
|
||||
textContent: boolean;
|
||||
tags: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export default function FilterSearchDropdown({
|
||||
setFilterDropdown,
|
||||
setSearchFilter,
|
||||
searchFilter,
|
||||
}: Props) {
|
||||
return (
|
||||
<ClickAwayHandler
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "filter-dropdown") setFilterDropdown(false);
|
||||
}}
|
||||
className="absolute top-8 right-0 border border-sky-100 dark:border-neutral-700 shadow-md bg-gray-50 dark:bg-neutral-800 rounded-md p-2 z-20 w-40"
|
||||
>
|
||||
<p className="mb-2 text-black dark:text-white text-center font-semibold">
|
||||
Filter by
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Checkbox
|
||||
label="Name"
|
||||
state={searchFilter.name}
|
||||
onClick={() =>
|
||||
setSearchFilter({ ...searchFilter, name: !searchFilter.name })
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Link"
|
||||
state={searchFilter.url}
|
||||
onClick={() =>
|
||||
setSearchFilter({ ...searchFilter, url: !searchFilter.url })
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Description"
|
||||
state={searchFilter.description}
|
||||
onClick={() =>
|
||||
setSearchFilter({
|
||||
...searchFilter,
|
||||
description: !searchFilter.description,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Tags"
|
||||
state={searchFilter.tags}
|
||||
onClick={() =>
|
||||
setSearchFilter({ ...searchFilter, tags: !searchFilter.tags })
|
||||
}
|
||||
/>
|
||||
<div className="dropdown dropdown-bottom dropdown-end">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-sm btn-square btn-ghost"
|
||||
>
|
||||
<i className="bi-funnel text-neutral text-2xl"></i>
|
||||
</div>
|
||||
</ClickAwayHandler>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mt-1">
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="search-filter-checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={searchFilter.name}
|
||||
onChange={() => {
|
||||
setSearchFilter({ ...searchFilter, name: !searchFilter.name });
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">Name</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="search-filter-checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={searchFilter.url}
|
||||
onChange={() => {
|
||||
setSearchFilter({ ...searchFilter, url: !searchFilter.url });
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">Link</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="search-filter-checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={searchFilter.description}
|
||||
onChange={() => {
|
||||
setSearchFilter({
|
||||
...searchFilter,
|
||||
description: !searchFilter.description,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">Description</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="search-filter-checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={searchFilter.textContent}
|
||||
onChange={() => {
|
||||
setSearchFilter({
|
||||
...searchFilter,
|
||||
textContent: !searchFilter.textContent,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">Full Content</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="search-filter-checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={searchFilter.tags}
|
||||
onChange={() => {
|
||||
setSearchFilter({
|
||||
...searchFilter,
|
||||
tags: !searchFilter.tags,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">Tags</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Select from "react-select";
|
||||
import { styles } from "./styles";
|
||||
import { Options } from "./types";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
|
||||
type Props = {
|
||||
onChange: any;
|
||||
@@ -43,8 +43,8 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) {
|
||||
}, [collections]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
isClearable
|
||||
<CreatableSelect
|
||||
isClearable={false}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
onChange={onChange}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
|
||||
|
||||
return (
|
||||
<CreatableSelect
|
||||
isClearable
|
||||
isClearable={false}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
onChange={onChange}
|
||||
|
||||
@@ -8,20 +8,27 @@ export const styles: StylesConfig = {
|
||||
...styles,
|
||||
fontFamily: font,
|
||||
cursor: "pointer",
|
||||
backgroundColor: state.isSelected ? "#0ea5e9" : "inherit",
|
||||
backgroundColor: state.isSelected ? "oklch(var(--p))" : "inherit",
|
||||
"&:hover": {
|
||||
backgroundColor: state.isSelected ? "#0ea5e9" : "#e2e8f0",
|
||||
backgroundColor: state.isSelected
|
||||
? "oklch(var(--p))"
|
||||
: "oklch(var(--nc))",
|
||||
},
|
||||
transition: "all 50ms",
|
||||
}),
|
||||
control: (styles) => ({
|
||||
control: (styles, state) => ({
|
||||
...styles,
|
||||
fontFamily: font,
|
||||
border: "none",
|
||||
borderRadius: "0.375rem",
|
||||
border: state.isFocused
|
||||
? "1px solid oklch(var(--p))"
|
||||
: "1px solid oklch(var(--nc))",
|
||||
boxShadow: "none",
|
||||
minHeight: "2.6rem",
|
||||
}),
|
||||
container: (styles) => ({
|
||||
container: (styles, state) => ({
|
||||
...styles,
|
||||
border: "1px solid #e0f2fe",
|
||||
height: "full",
|
||||
borderRadius: "0.375rem",
|
||||
lineHeight: "1.25rem",
|
||||
// "@media screen and (min-width: 1024px)": {
|
||||
@@ -36,10 +43,6 @@ export const styles: StylesConfig = {
|
||||
...styles,
|
||||
cursor: "pointer",
|
||||
}),
|
||||
clearIndicator: (styles) => ({
|
||||
...styles,
|
||||
visibility: "hidden",
|
||||
}),
|
||||
placeholder: (styles) => ({
|
||||
...styles,
|
||||
borderColor: "black",
|
||||
@@ -62,4 +65,5 @@ export const styles: StylesConfig = {
|
||||
backgroundColor: "#38bdf8",
|
||||
},
|
||||
}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
};
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import {
|
||||
CollectionIncludingMembersAndLinkCount,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import {
|
||||
faFolder,
|
||||
faEllipsis,
|
||||
faLink,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Dropdown from "./Dropdown";
|
||||
import useLinkStore from "@/store/links";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useAccountStore from "@/store/account";
|
||||
import useModalStore from "@/store/modals";
|
||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import { toast } from "react-hot-toast";
|
||||
import isValidUrl from "@/lib/client/isValidUrl";
|
||||
import Link from "next/link";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
count: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function LinkCard({ link, count, className }: Props) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const permissions = usePermissions(link.collection.id as number);
|
||||
|
||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const { links } = useLinkStore();
|
||||
|
||||
const { account } = useAccountStore();
|
||||
|
||||
let shortendURL;
|
||||
|
||||
try {
|
||||
shortendURL = new URL(link.url).host.toLowerCase();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
const [collection, setCollection] =
|
||||
useState<CollectionIncludingMembersAndLinkCount>(
|
||||
collections.find(
|
||||
(e) => e.id === link.collection.id
|
||||
) as CollectionIncludingMembersAndLinkCount
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCollection(
|
||||
collections.find(
|
||||
(e) => e.id === link.collection.id
|
||||
) as CollectionIncludingMembersAndLinkCount
|
||||
);
|
||||
}, [collections, links]);
|
||||
|
||||
const { removeLink, updateLink } = useLinkStore();
|
||||
|
||||
const pinLink = async () => {
|
||||
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
|
||||
|
||||
const load = toast.loading("Applying...");
|
||||
|
||||
setExpandDropdown(false);
|
||||
|
||||
const response = await updateLink({
|
||||
...link,
|
||||
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
|
||||
});
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok &&
|
||||
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
|
||||
};
|
||||
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading("Deleting...");
|
||||
|
||||
const response = await removeLink(link);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok && toast.success(`Link Deleted.`);
|
||||
setExpandDropdown(false);
|
||||
};
|
||||
|
||||
const url = isValidUrl(link.url) ? new URL(link.url) : undefined;
|
||||
|
||||
const formattedDate = new Date(link.createdAt as string).toLocaleString(
|
||||
"en-US",
|
||||
{
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-fit border border-solid border-sky-100 dark:border-neutral-700 bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none duration-100 rounded-2xl relative group ${className}`}
|
||||
>
|
||||
{(permissions === true ||
|
||||
permissions?.canUpdate ||
|
||||
permissions?.canDelete) && (
|
||||
<div
|
||||
onClick={() => setExpandDropdown(!expandDropdown)}
|
||||
id={"expand-dropdown" + link.id}
|
||||
className="text-gray-500 dark:text-gray-300 inline-flex rounded-md cursor-pointer hover:bg-slate-200 dark:hover:bg-neutral-700 absolute right-5 top-5 z-10 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
title="More"
|
||||
className="w-5 h-5"
|
||||
id={"expand-dropdown" + link.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
setModal({
|
||||
modal: "LINK",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwnerOrMod:
|
||||
permissions === true || (permissions?.canUpdate as boolean),
|
||||
active: link,
|
||||
});
|
||||
}}
|
||||
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-5"
|
||||
>
|
||||
{url && (
|
||||
<Image
|
||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
||||
width={64}
|
||||
height={64}
|
||||
alt=""
|
||||
className="blur-sm absolute w-16 group-hover:opacity-80 duration-100 rounded-2xl bottom-5 right-5 opacity-60 select-none"
|
||||
draggable="false"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
target.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between gap-5 w-full h-full z-0">
|
||||
<div className="flex flex-col justify-between w-full">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||
{count + 1}
|
||||
</p>
|
||||
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
|
||||
{unescapeString(link.name || link.description)}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/collections/${link.collection.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit my-3 hover:opacity-70 duration-100"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
className="w-4 h-4 mt-1 drop-shadow"
|
||||
style={{ color: collection?.color }}
|
||||
/>
|
||||
<p className="text-black dark:text-white truncate capitalize w-full">
|
||||
{collection?.name}
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit text-gray-500 dark:text-gray-300 hover:opacity-70 duration-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
|
||||
<p className="truncate w-full">{shortendURL}</p>
|
||||
</Link>
|
||||
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||
<p>{formattedDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{expandDropdown ? (
|
||||
<Dropdown
|
||||
items={[
|
||||
permissions === true
|
||||
? {
|
||||
name:
|
||||
link?.pinnedBy && link.pinnedBy[0]
|
||||
? "Unpin"
|
||||
: "Pin to Dashboard",
|
||||
onClick: pinLink,
|
||||
}
|
||||
: undefined,
|
||||
permissions === true || permissions?.canUpdate
|
||||
? {
|
||||
name: "Edit",
|
||||
onClick: () => {
|
||||
setModal({
|
||||
modal: "LINK",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwnerOrMod:
|
||||
permissions === true || permissions?.canUpdate,
|
||||
active: link,
|
||||
defaultIndex: 1,
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
permissions === true || permissions?.canDelete
|
||||
? {
|
||||
name: "Delete",
|
||||
onClick: deleteLink,
|
||||
}
|
||||
: undefined,
|
||||
]}
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "expand-dropdown" + link.id)
|
||||
setExpandDropdown(false);
|
||||
}}
|
||||
className="absolute top-12 right-5 w-36"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import LinkCard from "@/components/LinkViews/LinkCard";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
|
||||
export default function CardView({
|
||||
links,
|
||||
}: {
|
||||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
}) {
|
||||
return (
|
||||
<div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{links.map((e, i) => {
|
||||
return <LinkCard key={i} link={e} count={i} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import LinkGrid from "@/components/LinkViews/LinkGrid";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
|
||||
export default function GridView({
|
||||
links,
|
||||
}: {
|
||||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
}) {
|
||||
return (
|
||||
<div className="grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5">
|
||||
{links.map((e, i) => {
|
||||
return <LinkGrid link={e} count={i} key={i} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import LinkList from "@/components/LinkViews/LinkList";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
|
||||
export default function ListView({
|
||||
links,
|
||||
}: {
|
||||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{links.map((e, i) => {
|
||||
return <LinkList key={i} link={e} count={i} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
ArchivedFormat,
|
||||
CollectionIncludingMembersAndLinkCount,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} 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";
|
||||
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 LinkGroupedIconURL from "./LinkComponents/LinkGroupedIconURL";
|
||||
import useOnScreen from "@/hooks/useOnScreen";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
count: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function LinkGrid({ link, count, className }: Props) {
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const { links, getLink } = useLinkStore();
|
||||
|
||||
let shortendURL;
|
||||
|
||||
try {
|
||||
shortendURL = new URL(link.url || "").host.toLowerCase();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
const [collection, setCollection] =
|
||||
useState<CollectionIncludingMembersAndLinkCount>(
|
||||
collections.find(
|
||||
(e) => e.id === link.collection.id
|
||||
) as CollectionIncludingMembersAndLinkCount
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCollection(
|
||||
collections.find(
|
||||
(e) => e.id === link.collection.id
|
||||
) as CollectionIncludingMembersAndLinkCount
|
||||
);
|
||||
}, [collections, links]);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isVisible = useOnScreen(ref);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: any;
|
||||
|
||||
if (
|
||||
isVisible &&
|
||||
!link.preview?.startsWith("archives") &&
|
||||
link.preview !== "unavailable"
|
||||
) {
|
||||
interval = setInterval(async () => {
|
||||
getLink(link.id as number);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, [isVisible, link.preview]);
|
||||
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative"
|
||||
>
|
||||
<Link
|
||||
href={link.url || ""}
|
||||
target="_blank"
|
||||
className="rounded-2xl cursor-pointer"
|
||||
>
|
||||
<div className="relative rounded-t-2xl h-40 overflow-hidden">
|
||||
{previewAvailable(link) ? (
|
||||
<Image
|
||||
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
|
||||
width={1280}
|
||||
height={720}
|
||||
alt=""
|
||||
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow opacity-80 scale-105"
|
||||
style={{ filter: "blur(2px)" }}
|
||||
draggable="false"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
target.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : link.preview === "unavailable" ? (
|
||||
<div className="bg-gray-50 duration-100 h-40 bg-opacity-80"></div>
|
||||
) : (
|
||||
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
|
||||
)}
|
||||
<div
|
||||
style={
|
||||
{
|
||||
// background:
|
||||
// "radial-gradient(circle, rgba(255, 255, 255, 0.5), transparent)",
|
||||
}
|
||||
}
|
||||
className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md"
|
||||
>
|
||||
<LinkIcon link={link} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="divider my-0 last:hidden border-t border-neutral-content h-[1px]" />
|
||||
|
||||
<div className="p-3 mt-1">
|
||||
<p className="truncate w-full pr-8 text-primary">
|
||||
{unescapeString(link.name || link.description) || link.url}
|
||||
</p>
|
||||
|
||||
<div title={link.url || ""} className="w-fit">
|
||||
<div className="flex gap-1 item-center select-none text-neutral mt-1">
|
||||
<i className="bi-link-45deg text-lg mt-[0.15rem] leading-none"></i>
|
||||
<p className="text-sm truncate">{shortendURL}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
|
||||
|
||||
<div className="flex justify-between text-xs text-neutral px-3 pb-1">
|
||||
<div className="cursor-pointer w-fit">
|
||||
{collection ? (
|
||||
<LinkCollection link={link} collection={collection} />
|
||||
) : undefined}
|
||||
</div>
|
||||
<LinkDate link={link} />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{showInfo ? (
|
||||
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-2xl fade-in overflow-y-auto">
|
||||
<div
|
||||
onClick={() => setShowInfo(!showInfo)}
|
||||
className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
|
||||
>
|
||||
<i className="bi-x text-neutral text-2xl"></i>
|
||||
</div>
|
||||
<p className="text-neutral text-lg font-semibold">Description</p>
|
||||
|
||||
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
|
||||
<p>
|
||||
{link.description ? (
|
||||
unescapeString(link.description)
|
||||
) : (
|
||||
<span className="text-neutral text-sm">
|
||||
No description provided.
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{link.tags[0] ? (
|
||||
<>
|
||||
<p className="text-neutral text-lg mt-3 font-semibold">Tags</p>
|
||||
|
||||
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
|
||||
|
||||
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
|
||||
<div className="flex gap-1 items-center flex-wrap">
|
||||
{link.tags.map((e, i) => (
|
||||
<Link
|
||||
href={"/tags/" + e.id}
|
||||
key={i}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||
>
|
||||
#{e.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : undefined}
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
<LinkActions
|
||||
link={link}
|
||||
collection={collection}
|
||||
position="top-[10.75rem] right-3"
|
||||
toggleShowInfo={() => setShowInfo(!showInfo)}
|
||||
linkInfo={showInfo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
CollectionIncludingMembersAndLinkCount,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
collection: CollectionIncludingMembersAndLinkCount;
|
||||
position?: string;
|
||||
toggleShowInfo?: () => void;
|
||||
linkInfo?: boolean;
|
||||
};
|
||||
|
||||
export default function LinkActions({
|
||||
link,
|
||||
toggleShowInfo,
|
||||
position,
|
||||
linkInfo,
|
||||
}: Props) {
|
||||
const permissions = usePermissions(link.collection.id as number);
|
||||
|
||||
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||
|
||||
const { account } = useAccountStore();
|
||||
|
||||
const { removeLink, updateLink } = useLinkStore();
|
||||
|
||||
const pinLink = async () => {
|
||||
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
|
||||
|
||||
const load = toast.loading("Applying...");
|
||||
|
||||
const response = await updateLink({
|
||||
...link,
|
||||
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
|
||||
});
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok &&
|
||||
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
|
||||
};
|
||||
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading("Deleting...");
|
||||
|
||||
const response = await removeLink(link.id as number);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok && toast.success(`Link Deleted.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`dropdown dropdown-left absolute ${
|
||||
position || "top-3 right-3"
|
||||
} z-20`}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||
>
|
||||
<i title="More" className="bi-three-dots text-xl" />
|
||||
</div>
|
||||
<ul className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1">
|
||||
{permissions === true ? (
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
pinLink();
|
||||
}}
|
||||
>
|
||||
{link?.pinnedBy && link.pinnedBy[0]
|
||||
? "Unpin"
|
||||
: "Pin to Dashboard"}
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
{linkInfo !== undefined && toggleShowInfo ? (
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
toggleShowInfo();
|
||||
}}
|
||||
>
|
||||
{!linkInfo ? "Show" : "Hide"} Link Details
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
{permissions === true || permissions?.canUpdate ? (
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditLinkModal(true);
|
||||
}}
|
||||
>
|
||||
Edit Link
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setPreservedFormatsModal(true);
|
||||
}}
|
||||
>
|
||||
Preserved Formats
|
||||
</div>
|
||||
</li>
|
||||
{permissions === true || permissions?.canDelete ? (
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e) => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{editLinkModal ? (
|
||||
<EditLinkModal
|
||||
onClose={() => setEditLinkModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
{deleteLinkModal ? (
|
||||
<DeleteLinkModal
|
||||
onClose={() => setDeleteLinkModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
{preservedFormatsModal ? (
|
||||
<PreservedFormatsModal
|
||||
onClose={() => setPreservedFormatsModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
{/* {expandedLink ? (
|
||||
<ExpandedLink onClose={() => setExpandedLink(false)} link={link} />
|
||||
) : undefined} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
CollectionIncludingMembersAndLinkCount,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export default function LinkCollection({
|
||||
link,
|
||||
collection,
|
||||
}: {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
collection: CollectionIncludingMembersAndLinkCount;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
router.push(`/collections/${link.collection.id}`);
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
|
||||
title={collection?.name}
|
||||
>
|
||||
<i
|
||||
className="bi-folder-fill text-lg drop-shadow"
|
||||
style={{ color: collection?.color }}
|
||||
></i>
|
||||
<p className="truncate capitalize">{collection?.name}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import React from "react";
|
||||
|
||||
export default function LinkDate({
|
||||
link,
|
||||
}: {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
}) {
|
||||
const formattedDate = new Date(link.createdAt as string).toLocaleString(
|
||||
"en-US",
|
||||
{
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-neutral">
|
||||
<i className="bi-calendar3 text-lg"></i>
|
||||
<p>{formattedDate}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||