├── public ├── robots.txt ├── favicon.ico ├── apple-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-70x70.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-touch-icon.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── android-icon-144x144.png ├── android-icon-192x192.png ├── apple-icon-precomposed.png ├── site.webmanifest ├── browserconfig.xml └── manifest.json ├── app ├── application │ ├── enums │ │ ├── shared │ │ │ ├── Theme.ts │ │ │ ├── InputType.ts │ │ │ ├── ApplicationLayout.ts │ │ │ ├── Periodicity.ts │ │ │ ├── SvgIcon.ts │ │ │ └── Colors.ts │ │ ├── entities │ │ │ ├── EntityLimitType.ts │ │ │ ├── PropertyValueType.ts │ │ │ ├── PropertyType.ts │ │ │ └── PropertyCondition.ts │ │ ├── tenants │ │ │ ├── TenantUserType.ts │ │ │ ├── LinkedAccountStatus.ts │ │ │ ├── TenantUserStatus.ts │ │ │ └── TenantUserJoined.ts │ │ └── subscriptions │ │ │ ├── PricingModel.ts │ │ │ ├── SubscriptionPriceType.ts │ │ │ ├── SubscriptionBillingPeriod.ts │ │ │ └── SubscriptionFeatureLimitType.ts │ ├── dtos │ │ ├── shared │ │ │ └── FileBase64.ts │ │ ├── stats │ │ │ ├── StatChange.ts │ │ │ └── Stat.ts │ │ ├── data │ │ │ ├── SortedByDto.ts │ │ │ └── PaginationDto.ts │ │ ├── marketing │ │ │ ├── SocialProofDto.ts │ │ │ ├── NavbarItemDto.ts │ │ │ ├── TestimonialDto.ts │ │ │ └── MarketingFeatureDto.ts │ │ ├── entities │ │ │ ├── MediaDto.ts │ │ │ ├── RowDetailDto.ts │ │ │ ├── RowDynamicValuesDto.ts │ │ │ └── RowValueDto.ts │ │ ├── setup │ │ │ └── SetupItem.ts │ │ ├── email │ │ │ └── EmailTemplate.ts │ │ ├── layout │ │ │ └── Command.ts │ │ └── subscriptions │ │ │ ├── SubscriptionLimitDto.ts │ │ │ ├── SubscriptionFeatureDto.ts │ │ │ ├── PlanFeatureUsageDto.ts │ │ │ ├── SubscriptionProductDto.ts │ │ │ └── SubscriptionPriceDto.ts │ ├── Constants.ts │ └── sidebar │ │ ├── SidebarGroup.ts │ │ └── SidebarItem.ts ├── assets │ ├── img │ │ ├── icon-dark.png │ │ ├── logo-dark.png │ │ ├── icon-light.png │ │ ├── logo-light.png │ │ └── saasrock │ │ │ ├── icon-dark.png │ │ │ ├── icon-light.png │ │ │ ├── logo-dark.png │ │ │ └── logo-light.png │ ├── logos │ │ ├── prisma.png │ │ ├── stripe.png │ │ ├── formspree.png │ │ ├── postmark.png │ │ ├── convertkit.png │ │ └── tailwindcss.png │ └── features │ │ └── admin-portal.png ├── components │ ├── ui │ │ ├── Preview.tsx │ │ ├── pdf │ │ │ └── PreviewPdfViewers.tsx │ │ ├── icons │ │ │ ├── PlusIcon.tsx │ │ │ ├── SelectorIcon.tsx │ │ │ ├── PencilIcon.tsx │ │ │ ├── LockClosedIcon.tsx │ │ │ ├── ShareIcon.tsx │ │ │ ├── ClipboardFilledIcon.tsx │ │ │ ├── CheckIcon.tsx │ │ │ ├── RightIcon.tsx │ │ │ ├── ChevronDownIcon.tsx │ │ │ ├── PencilAltIcon.tsx │ │ │ ├── PaperClipIcon.tsx │ │ │ ├── ClipboardIcon.tsx │ │ │ ├── XIcon.tsx │ │ │ ├── DownloadIcon.tsx │ │ │ ├── ViewListIcon.tsx │ │ │ ├── TrashIcon.tsx │ │ │ ├── OptionsIcon.tsx │ │ │ ├── EyeIcon.tsx │ │ │ ├── RefreshIcon.tsx │ │ │ ├── GitHubIcon.tsx │ │ │ ├── VariableIcon.tsx │ │ │ └── OpenCloseArrowIcon.tsx │ │ ├── input │ │ │ ├── previews │ │ │ │ ├── PreviewInputText.tsx │ │ │ │ ├── date │ │ │ │ │ ├── PreviewInputDate.tsx │ │ │ │ │ └── PreviewInputDateWithState.tsx │ │ │ │ ├── text │ │ │ │ │ ├── PreviewInputText.tsx │ │ │ │ │ ├── PreviewInputTextWithHintAndHelp.tsx │ │ │ │ │ ├── PreviewInputTextWithAllOptions.tsx │ │ │ │ │ └── PreviewInputTextWithTranslation.tsx │ │ │ │ ├── number │ │ │ │ │ ├── PreviewInputNumber.tsx │ │ │ │ │ ├── PreviewInputNumberWithHintAndHelp.tsx │ │ │ │ │ └── PreviewInputNumberWithAllOptions.tsx │ │ │ │ ├── checkbox │ │ │ │ │ ├── PreviewInputCheckbox.tsx │ │ │ │ │ └── PreviewInputCheckboxWithState.tsx │ │ │ │ ├── selector │ │ │ │ │ ├── PreviewInputSelector.tsx │ │ │ │ │ ├── PreviewInputSelect.tsx │ │ │ │ │ ├── PreviewInputSelectWithState.tsx │ │ │ │ │ └── PreviewInputSelectorWithState.tsx │ │ │ │ └── radio │ │ │ │ │ ├── PreviewInputRadioGroup.tsx │ │ │ │ │ └── PreviewInputRadioGroupWithState.tsx │ │ │ ├── InputCheckboxWithDescription.tsx │ │ │ ├── InputSearch.tsx │ │ │ ├── InputSelect.tsx │ │ │ ├── InputDate.tsx │ │ │ └── InputCheckboxInline.tsx │ │ ├── loaders │ │ │ ├── PreviewLoaders.tsx │ │ │ ├── PreviewFloatingLoader.tsx │ │ │ └── Loading.tsx │ │ ├── tooltips │ │ │ ├── PreviewTooltips.tsx │ │ │ └── HintTooltip.tsx │ │ ├── badges │ │ │ ├── SimpleBadge.tsx │ │ │ ├── ColorBadge.tsx │ │ │ ├── PreviewSimpleBadges.tsx │ │ │ └── PreviewColorBadges.tsx │ │ ├── tabs │ │ │ ├── PreviewTabsAsLinks.tsx │ │ │ ├── PreviewTabsVertical.tsx │ │ │ ├── PreviewTabsAsButtons.tsx │ │ │ └── PreviewTabs.tsx │ │ ├── dividers │ │ │ └── Divider.tsx │ │ ├── SampleComponent.tsx │ │ ├── transitions │ │ │ └── EaseInAndOut.tsx │ │ ├── datepickers │ │ │ └── DateInputButton.tsx │ │ ├── forms │ │ │ └── InputGroup.tsx │ │ ├── dropdowns │ │ │ ├── PreviewDropdowns.tsx │ │ │ ├── DropdownOptions.tsx │ │ │ └── Dropdown.tsx │ │ ├── layouts │ │ │ ├── NewPageLayout.tsx │ │ │ ├── EditPageLayout.tsx │ │ │ └── IndexPageLayout.tsx │ │ ├── uploaders │ │ │ ├── PreviewUploadersDocument.tsx │ │ │ └── UploadImage.tsx │ │ ├── banners │ │ │ ├── PreviewBanners.tsx │ │ │ ├── InfoBanner.tsx │ │ │ └── WarningBanner.tsx │ │ ├── emptyState │ │ │ └── PreviewEmptyStates.tsx │ │ ├── logo-and-icon │ │ │ ├── PreviewIcon.tsx │ │ │ └── PreviewLogo.tsx │ │ ├── breadcrumbs │ │ │ ├── PreviewBreadcrumbs.tsx │ │ │ └── BreadcrumbSimple.tsx │ │ ├── commandPalettes │ │ │ └── PreviewCommandPalettes.tsx │ │ ├── buttons │ │ │ ├── LoadingButton.tsx │ │ │ ├── PreviewButtonsAsLinks.tsx │ │ │ ├── PreviewButtonsDestructive.tsx │ │ │ ├── PreviewButtons.tsx │ │ │ ├── ButtonFlyout.tsx │ │ │ └── ButtonShare.tsx │ │ ├── images │ │ │ └── LogoClouds.tsx │ │ ├── tables │ │ │ ├── CollapsibleRow.tsx │ │ │ └── PreviewTableSimple.tsx │ │ ├── selectors │ │ │ └── LayoutSelector.tsx │ │ ├── media │ │ │ └── PreviewMediaModal.tsx │ │ └── modals │ │ │ ├── OpenModal.tsx │ │ │ └── Modal.tsx │ ├── blocks │ │ ├── BlocksBreadcrumb.tsx │ │ ├── BlockLoader.ts │ │ └── BlockDemoCodeToggle.tsx │ ├── front │ │ ├── Icon.tsx │ │ └── Logo.tsx │ ├── app │ │ └── AppLayout.tsx │ └── pages │ │ ├── Page404.tsx │ │ └── Page401.tsx ├── utils │ ├── shared │ │ ├── ClassesUtils.ts │ │ ├── StringUtils.ts │ │ ├── NumberUtils.ts │ │ ├── ObjectUtils.ts │ │ ├── CommandUtils.ts │ │ ├── UrlUtils.ts │ │ ├── KeypressUtils.ts │ │ └── DateUtils.ts │ ├── db.server.ts │ └── data │ │ └── useRootData.ts ├── locale │ ├── supportedLocales.ts │ ├── i18n.utils.ts │ ├── i18n.custom.server.ts │ └── i18n.server.ts ├── routes │ ├── components.tsx │ ├── components │ │ ├── tooltips.mdx │ │ ├── logo-and-icon.mdx │ │ ├── pdf-viewer.mdx │ │ ├── inputs │ │ │ ├── index.mdx │ │ │ ├── date.mdx │ │ │ ├── checkbox.mdx │ │ │ ├── radio-group.mdx │ │ │ └── number.mdx │ │ ├── badges.mdx │ │ ├── modals.mdx │ │ ├── loaders.mdx │ │ ├── uploaders.mdx │ │ ├── empty-states.mdx │ │ ├── command-palette.mdx │ │ ├── index.mdx │ │ ├── buttons.mdx │ │ ├── forms.mdx │ │ ├── banners.mdx │ │ ├── tabs.mdx │ │ ├── breadcrumbs.mdx │ │ ├── dropdowns.mdx │ │ └── tables.mdx │ ├── 404.tsx │ ├── 401.tsx │ ├── blocks │ │ ├── forms │ │ │ ├── simple-form.tsx │ │ │ ├── form-with-all-input-types.tsx │ │ │ ├── form-with-confirmation-dialog.tsx │ │ │ └── multiple-forms-on-one-route.tsx │ │ ├── lists │ │ │ ├── table.tsx │ │ │ └── table │ │ │ │ ├── index.tsx │ │ │ │ └── code.mdx │ │ ├── email │ │ │ ├── newsletter-with-convertkit.tsx │ │ │ ├── contact-form-with-formspree.tsx │ │ │ ├── send-email-with-postmark-template.tsx │ │ │ └── contact-form-with-formspree │ │ │ │ ├── index.tsx │ │ │ │ └── code.mdx │ │ └── subscriptions │ │ │ └── create-pricing-plans-with-stripe.tsx │ └── index.mdx ├── entry.client.tsx └── entry.server.tsx ├── .github └── FUNDING.yml ├── remix.env.d.ts ├── vercel.json ├── postcss.config.js ├── .eslintrc.js ├── .env.example ├── .gitignore ├── tsconfig.json ├── remix.config.js ├── LICENSE ├── README.md └── styles └── app.css /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /app/application/enums/shared/Theme.ts: -------------------------------------------------------------------------------- 1 | export enum Theme { 2 | LIGHT, 3 | DARK, 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: alexandromtzg 4 | -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /app/application/dtos/shared/FileBase64.ts: -------------------------------------------------------------------------------- 1 | export interface FileBase64 { 2 | file: File; 3 | base64: any; 4 | } 5 | -------------------------------------------------------------------------------- /app/application/enums/entities/EntityLimitType.ts: -------------------------------------------------------------------------------- 1 | export enum EntityLimitType { 2 | MAX, 3 | MONTHLY, 4 | } 5 | -------------------------------------------------------------------------------- /app/application/enums/shared/InputType.ts: -------------------------------------------------------------------------------- 1 | export enum InputType { 2 | TEXT, 3 | NUMBER, 4 | SELECT, 5 | } 6 | -------------------------------------------------------------------------------- /app/assets/img/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/icon-dark.png -------------------------------------------------------------------------------- /app/assets/img/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/logo-dark.png -------------------------------------------------------------------------------- /app/assets/logos/prisma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/logos/prisma.png -------------------------------------------------------------------------------- /app/assets/logos/stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/logos/stripe.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "env": { 4 | "ENABLE_FILE_SYSTEM_API": "1" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/application/Constants.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_PAGE_SIZE = 9; 2 | 3 | export default { 4 | DEFAULT_PAGE_SIZE, 5 | }; 6 | -------------------------------------------------------------------------------- /app/application/dtos/stats/StatChange.ts: -------------------------------------------------------------------------------- 1 | export enum StatChange { 2 | Decrease, 3 | Equal, 4 | Increase, 5 | } 6 | -------------------------------------------------------------------------------- /app/application/enums/shared/ApplicationLayout.ts: -------------------------------------------------------------------------------- 1 | export enum ApplicationLayout { 2 | SIDEBAR, 3 | STACKED, 4 | } 5 | -------------------------------------------------------------------------------- /app/assets/img/icon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/icon-light.png -------------------------------------------------------------------------------- /app/assets/img/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/logo-light.png -------------------------------------------------------------------------------- /app/assets/logos/formspree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/logos/formspree.png -------------------------------------------------------------------------------- /app/assets/logos/postmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/logos/postmark.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /app/application/enums/tenants/TenantUserType.ts: -------------------------------------------------------------------------------- 1 | export enum TenantUserType { 2 | OWNER, 3 | ADMIN, 4 | MEMBER, 5 | } 6 | -------------------------------------------------------------------------------- /app/assets/logos/convertkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/logos/convertkit.png -------------------------------------------------------------------------------- /app/assets/logos/tailwindcss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/logos/tailwindcss.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /app/application/dtos/data/SortedByDto.ts: -------------------------------------------------------------------------------- 1 | export interface SortedByDto { 2 | name: string; 3 | direction: "asc" | "desc"; 4 | } 5 | -------------------------------------------------------------------------------- /app/assets/features/admin-portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/features/admin-portal.png -------------------------------------------------------------------------------- /app/assets/img/saasrock/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/saasrock/icon-dark.png -------------------------------------------------------------------------------- /app/assets/img/saasrock/icon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/saasrock/icon-light.png -------------------------------------------------------------------------------- /app/assets/img/saasrock/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/saasrock/logo-dark.png -------------------------------------------------------------------------------- /app/assets/img/saasrock/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexandroMtzG/remix-blocks/HEAD/app/assets/img/saasrock/logo-light.png -------------------------------------------------------------------------------- /app/components/ui/Preview.tsx: -------------------------------------------------------------------------------- 1 | export default function Preview() { 2 | return
{/* */}
; 3 | } 4 | -------------------------------------------------------------------------------- /app/application/enums/entities/PropertyValueType.ts: -------------------------------------------------------------------------------- 1 | export enum PropertyValueType { 2 | ID, 3 | NUMBER, 4 | TEXT, 5 | DATE, 6 | } 7 | -------------------------------------------------------------------------------- /app/application/enums/subscriptions/PricingModel.ts: -------------------------------------------------------------------------------- 1 | export enum PricingModel { 2 | FLAT_RATE, 3 | PER_SEAT, 4 | USAGE_BASED, 5 | } 6 | -------------------------------------------------------------------------------- /app/application/enums/subscriptions/SubscriptionPriceType.ts: -------------------------------------------------------------------------------- 1 | export enum SubscriptionPriceType { 2 | ONE_TIME, 3 | RECURRING, 4 | } 5 | -------------------------------------------------------------------------------- /app/application/enums/tenants/LinkedAccountStatus.ts: -------------------------------------------------------------------------------- 1 | export enum LinkedAccountStatus { 2 | PENDING, 3 | LINKED, 4 | REJECTED, 5 | } 6 | -------------------------------------------------------------------------------- /app/utils/shared/ClassesUtils.ts: -------------------------------------------------------------------------------- 1 | export default function clsx(...classes: any[]) { 2 | return classes.filter(Boolean).join(" "); 3 | } 4 | -------------------------------------------------------------------------------- /app/application/dtos/marketing/SocialProofDto.ts: -------------------------------------------------------------------------------- 1 | export type SocialProofDto = { 2 | totalDownloads?: number; 3 | totalMembers?: number; 4 | }; 5 | -------------------------------------------------------------------------------- /app/application/dtos/entities/MediaDto.ts: -------------------------------------------------------------------------------- 1 | export type MediaDto = { 2 | title: string; 3 | name: string; 4 | file: string; 5 | type: string; 6 | }; 7 | -------------------------------------------------------------------------------- /app/application/enums/shared/Periodicity.ts: -------------------------------------------------------------------------------- 1 | export enum Periodicity { 2 | ONCE, 3 | MONTHLY, 4 | BIMONTHLY, 5 | QUARTERLY, 6 | YEARLY, 7 | } 8 | -------------------------------------------------------------------------------- /app/application/dtos/setup/SetupItem.ts: -------------------------------------------------------------------------------- 1 | export type SetupItem = { 2 | title: string; 3 | description: string; 4 | completed: boolean; 5 | path: string; 6 | }; 7 | -------------------------------------------------------------------------------- /app/application/enums/tenants/TenantUserStatus.ts: -------------------------------------------------------------------------------- 1 | export enum TenantUserStatus { 2 | PENDING_INVITATION, 3 | PENDING_ACCEPTANCE, 4 | ACTIVE, 5 | INACTIVE, 6 | } 7 | -------------------------------------------------------------------------------- /app/application/enums/subscriptions/SubscriptionBillingPeriod.ts: -------------------------------------------------------------------------------- 1 | export enum SubscriptionBillingPeriod { 2 | ONCE, 3 | DAILY, 4 | WEEKLY, 5 | MONTHLY, 6 | YEARLY, 7 | } 8 | -------------------------------------------------------------------------------- /app/application/enums/tenants/TenantUserJoined.ts: -------------------------------------------------------------------------------- 1 | export enum TenantUserJoined { 2 | CREATOR, 3 | JOINED_BY_INVITATION, 4 | JOINED_BY_LINK, 5 | JOINED_BY_PUBLIC_URL, 6 | } 7 | -------------------------------------------------------------------------------- /app/locale/supportedLocales.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | lang: "en", 4 | flag: "us", 5 | }, 6 | { 7 | lang: "es", 8 | flag: "mx", 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /app/application/sidebar/SidebarGroup.ts: -------------------------------------------------------------------------------- 1 | import { SideBarItem } from "./SidebarItem"; 2 | 3 | export interface SidebarGroup { 4 | title: string; 5 | items: SideBarItem[]; 6 | } 7 | -------------------------------------------------------------------------------- /app/application/enums/subscriptions/SubscriptionFeatureLimitType.ts: -------------------------------------------------------------------------------- 1 | export enum SubscriptionFeatureLimitType { 2 | NOT_INCLUDED, 3 | INCLUDED, 4 | MONTHLY, 5 | MAX, 6 | UNLIMITED, 7 | } 8 | -------------------------------------------------------------------------------- /app/application/dtos/marketing/NavbarItemDto.ts: -------------------------------------------------------------------------------- 1 | export interface NavbarItemDto { 2 | title: string; 3 | path?: string; 4 | description?: string; 5 | className?: string; 6 | items?: NavbarItemDto[]; 7 | } 8 | -------------------------------------------------------------------------------- /app/application/dtos/entities/RowDetailDto.ts: -------------------------------------------------------------------------------- 1 | import { RowValueDto } from "./RowValueDto"; 2 | 3 | export type RowDetailDto = { 4 | id: string | null; 5 | folio: number | null; 6 | values: RowValueDto[]; 7 | }; 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@remix-run/eslint-config"], 3 | rules: { 4 | "no-console": "warn", 5 | "import/first": "off", 6 | "@typescript-eslint/consistent-type-imports": "off", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /app/application/enums/entities/PropertyType.ts: -------------------------------------------------------------------------------- 1 | export enum PropertyType { 2 | NUMBER, 3 | TEXT, 4 | DATE, 5 | USER, 6 | ROLE, 7 | ENTITY, 8 | MEDIA, 9 | ID, 10 | SELECT, 11 | FORMULA, 12 | BOOLEAN, 13 | } 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="RemixBlocks" 2 | SESSION_SECRET=abc123 3 | SERVER_URL=http://localhost:3000 4 | INTEGRATIONS_CONTACT_FORMSPREE= 5 | CONVERTKIT_APIKEY= 6 | CONVERTKIT_FORM_ID= 7 | POSTMARK_SERVER_TOKEN= 8 | POSTMARK_FROM_EMAIL= 9 | STRIPE_SK= -------------------------------------------------------------------------------- /app/application/enums/entities/PropertyCondition.ts: -------------------------------------------------------------------------------- 1 | export enum PropertyCondition { 2 | EQUAL, // = 3 | GREATER_THAN, // > 4 | EQUAL_OR_GREATER_THAN, // >= 5 | LESS_THAN, // < 6 | EQUAL_OR_LESS_THAN, // <=, 7 | // CONTAINS, // (), 8 | } 9 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /app/application/dtos/stats/Stat.ts: -------------------------------------------------------------------------------- 1 | import { StatChange } from "./StatChange"; 2 | 3 | export type Stat = { 4 | name: string; 5 | hint: string; 6 | stat: string; 7 | previousStat: string; 8 | change: string; 9 | changeType: StatChange; 10 | path?: string; 11 | }; 12 | -------------------------------------------------------------------------------- /app/application/dtos/email/EmailTemplate.ts: -------------------------------------------------------------------------------- 1 | export type EmailTemplate = { 2 | type: "standard" | "layout"; 3 | name: string; 4 | alias: string; 5 | subject: string; 6 | htmlBody: string; 7 | active: boolean; 8 | associatedServerId: number; 9 | templateId: number; 10 | }; 11 | -------------------------------------------------------------------------------- /app/application/dtos/data/PaginationDto.ts: -------------------------------------------------------------------------------- 1 | import { SortedByDto } from "./SortedByDto"; 2 | 3 | export interface PaginationDto { 4 | page: number; 5 | pageSize: number; 6 | totalItems: number; 7 | totalPages: number; 8 | sortedBy?: SortedByDto; 9 | query?: string | undefined; 10 | } 11 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /app/application/dtos/layout/Command.ts: -------------------------------------------------------------------------------- 1 | export type Command = { 2 | title: string; 3 | command: string; 4 | description: string; 5 | bgClassName?: string; 6 | textClassName?: string; 7 | toPath?: string; 8 | adminOnly?: boolean; 9 | items?: Command[]; 10 | onSelected?: () => void; 11 | }; 12 | -------------------------------------------------------------------------------- /app/application/dtos/marketing/TestimonialDto.ts: -------------------------------------------------------------------------------- 1 | export interface TestimonialDto { 2 | company: string; 3 | companyUrl: string; 4 | logoLightMode?: string; 5 | logoDarkMode?: string; 6 | quote: string; 7 | name: string; 8 | personalWebsite?: string; 9 | avatar: string; 10 | role: string; 11 | } 12 | -------------------------------------------------------------------------------- /app/application/dtos/subscriptions/SubscriptionLimitDto.ts: -------------------------------------------------------------------------------- 1 | // import { SubscriptionLimitType } from "~/application/enums/subscriptions/SubscriptionLimitType"; 2 | 3 | // export interface SubscriptionLimitDto { 4 | // title: string; 5 | // name: string; 6 | // type: SubscriptionLimitType; 7 | // value: number; 8 | // } 9 | -------------------------------------------------------------------------------- /app/components/ui/pdf/PreviewPdfViewers.tsx: -------------------------------------------------------------------------------- 1 | import PdfViewer from "./PdfViewer"; 2 | import FakePdfBase64 from "./FakePdfBase64"; 3 | 4 | export default function PreviewPdfViewers() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/application/dtos/entities/RowDynamicValuesDto.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Property, RowValue } from "@prisma/client"; 2 | 3 | export interface RowDynamicValuesDto { 4 | form: Entity | undefined; 5 | name: string; 6 | total: number; 7 | headers: (RowValue & { property: Property })[]; 8 | // details: RequestDetailDto[]; 9 | } 10 | -------------------------------------------------------------------------------- /app/application/enums/shared/SvgIcon.ts: -------------------------------------------------------------------------------- 1 | export enum SvgIcon { 2 | ADMIN, 3 | TENANTS, 4 | USERS, 5 | PRICING, 6 | EMAILS, 7 | NAVIGATION, 8 | COMPONENTS, 9 | MEMBERS, 10 | PROFILE, 11 | APP, 12 | DASHBOARD, 13 | SETTINGS, 14 | SETUP, 15 | LOGS, 16 | BLOG, 17 | ENTITIES, 18 | KEYS, 19 | DOCS, 20 | LINKS, 21 | } 22 | -------------------------------------------------------------------------------- /app/application/dtos/subscriptions/SubscriptionFeatureDto.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionFeatureLimitType } from "~/application/enums/subscriptions/SubscriptionFeatureLimitType"; 2 | 3 | export interface SubscriptionFeatureDto { 4 | order: number; 5 | title: string; 6 | name: string; 7 | type: SubscriptionFeatureLimitType; 8 | value: number; 9 | } 10 | -------------------------------------------------------------------------------- /app/routes/components.tsx: -------------------------------------------------------------------------------- 1 | import styles from "highlight.js/styles/night-owl.css"; 2 | import { Outlet } from "@remix-run/react"; 3 | 4 | export const links = () => { 5 | return [ 6 | { 7 | rel: "stylesheet", 8 | href: styles, 9 | }, 10 | ]; 11 | }; 12 | 13 | export default function ComponentsRoute() { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /app/components/ui/icons/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function PlusIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/PreviewInputText.tsx: -------------------------------------------------------------------------------- 1 | import InputText from "../InputText"; 2 | 3 | export default function PreviewInputText() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .cache 4 | .env 5 | .vercel 6 | .output 7 | .vscode 8 | 9 | /build/ 10 | /public/build 11 | /api/index.js 12 | /api/index.js.map 13 | /app/styles/app.css 14 | 15 | # Supabase 16 | **/supabase/.branches 17 | **/supabase/.temp 18 | **/supabase/.env 19 | 20 | prisma/dev.db 21 | .env 22 | .env.production 23 | 24 | .idea 25 | api/_assets 26 | -------------------------------------------------------------------------------- /app/application/enums/shared/Colors.ts: -------------------------------------------------------------------------------- 1 | export enum Colors { 2 | UNDEFINED, 3 | SLATE, 4 | GRAY, 5 | NEUTRAL, 6 | STONE, 7 | RED, 8 | ORANGE, 9 | AMBER, 10 | YELLOW, 11 | LIME, 12 | GREEN, 13 | EMERALD, 14 | TEAL, 15 | CYAN, 16 | SKY, 17 | BLUE, 18 | INDIGO, 19 | VIOLET, 20 | PURPLE, 21 | FUCHSIA, 22 | PINK, 23 | ROSE, 24 | } 25 | -------------------------------------------------------------------------------- /app/components/blocks/BlocksBreadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import BreadcrumbSimple from "~/components/ui/breadcrumbs/BreadcrumbSimple"; 2 | 3 | interface Props { 4 | items: { title: string; routePath: string }[]; 5 | } 6 | 7 | export default function BlocksBreadcrumb({ items }: Props) { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /app/components/ui/icons/SelectorIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function SelectorIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/date/PreviewInputDate.tsx: -------------------------------------------------------------------------------- 1 | import InputDate from "~/components/ui/input/InputDate"; 2 | 3 | export default function PreviewInputDate() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/text/PreviewInputText.tsx: -------------------------------------------------------------------------------- 1 | import InputText from "~/components/ui/input/InputText"; 2 | 3 | export default function PreviewInputText() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/utils/shared/StringUtils.ts: -------------------------------------------------------------------------------- 1 | const toCamelCase = (value: string) => { 2 | return value.replace(/(? (+m === 0 ? "" : m.toUpperCase())).replace(/^./, (m) => m?.toLowerCase()); 3 | }; 4 | const capitalize = (str: string) => { 5 | return str.charAt(0).toUpperCase() + str.slice(1); 6 | }; 7 | 8 | export default { 9 | toCamelCase, 10 | capitalize, 11 | }; 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/PencilIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function PencilIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/application/sidebar/SidebarItem.ts: -------------------------------------------------------------------------------- 1 | import { SvgIcon } from "../enums/shared/SvgIcon"; 2 | import { ReactNode } from "react"; 3 | 4 | export interface SideBarItem { 5 | title: string; 6 | path: string; 7 | icon?: SvgIcon; 8 | description?: string; 9 | open?: boolean; 10 | items?: SideBarItem[]; 11 | side?: ReactNode; 12 | exact?: boolean; 13 | hideFromCommandPalette?: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/number/PreviewInputNumber.tsx: -------------------------------------------------------------------------------- 1 | import InputNumber from "~/components/ui/input/InputNumber"; 2 | 3 | export default function PreviewInputNumber() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/checkbox/PreviewInputCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import InputCheckbox from "~/components/ui/input/InputCheckbox"; 2 | 3 | export default function PreviewInputCheckbox() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/LockClosedIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function LockClosedIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/components/ui/icons/ShareIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ShareIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/components/ui/loaders/PreviewLoaders.tsx: -------------------------------------------------------------------------------- 1 | import Loading from "./Loading"; 2 | 3 | export default function PreviewLoaders() { 4 | return ( 5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/locale/i18n.utils.ts: -------------------------------------------------------------------------------- 1 | import { getUserInfo } from "~/utils/session.server"; 2 | import { i18n } from "./i18n.server"; 3 | 4 | export async function i18nHelper(request: Request) { 5 | const userInfo = await getUserInfo(request); 6 | let t = await i18n.getFixedT(request, "translations"); 7 | const translations = await i18n.getTranslations(userInfo.lng ?? request, ["translations"]); 8 | return { t, translations }; 9 | } 10 | -------------------------------------------------------------------------------- /app/application/dtos/subscriptions/PlanFeatureUsageDto.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionFeatureLimitType } from "~/application/enums/subscriptions/SubscriptionFeatureLimitType"; 2 | 3 | export interface PlanFeatureUsageDto { 4 | order: number; 5 | title: string; 6 | name: string; 7 | type: SubscriptionFeatureLimitType; 8 | value: number; 9 | used: number; 10 | remaining: number; 11 | enabled: boolean; 12 | message: string; 13 | } 14 | -------------------------------------------------------------------------------- /app/routes/components/tooltips.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Tooltips | Components | RemixBlocks - Help your users 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | # Tooltips 9 | 10 | import PreviewTooltips from "~/components/ui/tooltips/PreviewTooltips"; 11 | 12 | 13 | 14 | ```tsx 15 | 18 | ``` 19 | -------------------------------------------------------------------------------- /app/components/ui/icons/ClipboardFilledIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ClipboardFilledIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /app/components/ui/tooltips/PreviewTooltips.tsx: -------------------------------------------------------------------------------- 1 | import HintTooltip from "./HintTooltip"; 2 | 3 | export default function PreviewTooltips() { 4 | return ( 5 |
6 |
7 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/components/ui/icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function CheckIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/RightIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function RightIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/text/PreviewInputTextWithHintAndHelp.tsx: -------------------------------------------------------------------------------- 1 | import InputText from "~/components/ui/input/InputText"; 2 | 3 | export default function PreviewInputTextWithHint() { 4 | return ( 5 |
6 |
7 | Hint text} /> 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/ChevronDownIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ChevronDownIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/PencilAltIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function PencilAltIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/number/PreviewInputNumberWithHintAndHelp.tsx: -------------------------------------------------------------------------------- 1 | import InputNumber from "~/components/ui/input/InputNumber"; 2 | 3 | export default function PreviewInputNumberWithHintAndHelp() { 4 | return ( 5 |
6 |
7 | Hint text} help={"Help text"} /> 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/application/dtos/marketing/MarketingFeatureDto.ts: -------------------------------------------------------------------------------- 1 | export enum MarketingFeatureStatus { 2 | UnderReview, 3 | Planned, 4 | InProgress, 5 | Done, 6 | } 7 | export enum MarketingFeatureType { 8 | Core, 9 | Enterprise, 10 | } 11 | export interface MarketingFeatureDto { 12 | name: string; 13 | description: string; 14 | status?: MarketingFeatureStatus; 15 | type?: MarketingFeatureType; 16 | link?: string; 17 | save?: number; 18 | platforms?: { 19 | site?: string; 20 | price?: string; 21 | }[]; 22 | } 23 | -------------------------------------------------------------------------------- /app/components/ui/badges/SimpleBadge.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Colors } from "~/application/enums/shared/Colors"; 3 | import { getBadgeColor } from "~/utils/shared/ColorUtils"; 4 | 5 | interface Props { 6 | title: string; 7 | color: Colors; 8 | className?: string; 9 | } 10 | 11 | export default function SimpleBadge({ title, color, className }: Props) { 12 | return
{title}
; 13 | } 14 | -------------------------------------------------------------------------------- /app/components/ui/icons/PaperClipIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function PaperClipIcon({ className }: { className: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/tabs/PreviewTabsAsLinks.tsx: -------------------------------------------------------------------------------- 1 | import Tabs from "./Tabs"; 2 | 3 | export default function PreviewTabsAsLinks() { 4 | return ( 5 |
6 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ui/tabs/PreviewTabsVertical.tsx: -------------------------------------------------------------------------------- 1 | import TabsVertical from "./TabsVertical"; 2 | 3 | export default function PreviewTabsVertical() { 4 | return ( 5 |
6 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/locale/i18n.custom.server.ts: -------------------------------------------------------------------------------- 1 | import { Backend } from "remix-i18next"; 2 | 3 | export class FetchBackend implements Backend { 4 | private url: URL; 5 | 6 | constructor(url: string) { 7 | this.url = new URL(url); 8 | } 9 | 10 | async getTranslations(namespace: string, locale: string) { 11 | let url = new URL(`${locale}/${namespace}.json`, this.url); 12 | let response = await fetch(url.toString(), { 13 | headers: { accept: "application/json" }, 14 | }); 15 | return response.json(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/components/ui/icons/ClipboardIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ClipboardIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/XIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function XIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/DownloadIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function DownloadIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/ViewListIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ViewList({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/checkbox/PreviewInputCheckboxWithState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputCheckbox from "~/components/ui/input/InputCheckbox"; 3 | 4 | export default function PreviewInputCheckboxWithState() { 5 | const [value, setValue] = useState(true); 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/date/PreviewInputDateWithState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputDate from "~/components/ui/input/InputDate"; 3 | 4 | export default function PreviewInputDateWithState() { 5 | const [value, setValue] = useState(new Date("1990-01-02")); 6 | return ( 7 |
8 |
9 | setValue(e)} /> 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/routes/404.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction } from "@remix-run/node"; 2 | import Page404 from "~/components/pages/Page404"; 3 | import { i18nHelper } from "~/locale/i18n.utils"; 4 | 5 | export let loader: LoaderFunction = async ({ request }) => { 6 | let { t, translations } = await i18nHelper(request); 7 | return json({ 8 | title: `${t("shared.notFound")} | ${process.env.APP_NAME}`, 9 | i18n: translations, 10 | }); 11 | }; 12 | 13 | export default function Route404() { 14 | return ( 15 | <> 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | "noEmit": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "allowJs": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/components/ui/icons/TrashIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function TrashIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/icons/OptionsIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function OptionsIcon() { 2 | return ( 3 |
4 | 5 | 6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/components/ui/dividers/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | export default function Divider({ children }: Props) { 7 | return ( 8 |
9 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/number/PreviewInputNumberWithAllOptions.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputNumber from "~/components/ui/input/InputNumber"; 3 | 4 | export default function PreviewInputNumberWithAllOptions() { 5 | const [value, setValue] = useState(0); 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/text/PreviewInputTextWithAllOptions.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputText from "~/components/ui/input/InputText"; 3 | 4 | export default function PreviewInputTextWithAllOptions() { 5 | const [value, setValue] = useState(""); 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/db.server.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let db: PrismaClient; 4 | 5 | declare global { 6 | var __db: PrismaClient | undefined; 7 | } 8 | 9 | // this is needed because in development we don't want to restart 10 | // the server with every change, but we want to make sure we don't 11 | // create a new connection to the DB with every change either. 12 | if (process.env.NODE_ENV === "production") { 13 | db = new PrismaClient(); 14 | } else { 15 | if (!global.__db) { 16 | global.__db = new PrismaClient(); 17 | } 18 | db = global.__db; 19 | } 20 | 21 | export { db }; 22 | -------------------------------------------------------------------------------- /app/components/ui/SampleComponent.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary"; 3 | 4 | export default function SampleComponent() { 5 | const [counter, setCounter] = useState(0); 6 | 7 | useEffect(() => { 8 | // eslint-disable-next-line no-console 9 | console.log("mounted"); 10 | }, []); 11 | 12 | function increment(i: number) { 13 | setCounter(counter + i); 14 | } 15 | 16 | return ( 17 |
18 | increment(1)}>Counter: {counter} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/locale/i18n.server.ts: -------------------------------------------------------------------------------- 1 | import { FetchBackend, RemixI18Next } from "remix-i18next"; 2 | // You will need to provide a backend to load your translations, here we use the 3 | // file system one and tell it where to find the translations. 4 | let backend = new FetchBackend({ 5 | baseUrl: new URL(process.env.SERVER_URL?.toString() ?? ""), 6 | pathPattern: "/locales/:locale/:namespace.json", 7 | }); 8 | 9 | export let i18n = new RemixI18Next(backend, { 10 | fallbackLng: "en", // here configure your default (fallback) language 11 | supportedLanguages: ["es", "en"], // here configure your supported languages 12 | }); 13 | -------------------------------------------------------------------------------- /app/utils/shared/NumberUtils.ts: -------------------------------------------------------------------------------- 1 | import numeral from "numeral"; 2 | 3 | const numberFormat = (value: number): string => { 4 | return numeral(value).format("0,0"); 5 | }; 6 | const decimalFormat = (value: number): string => { 7 | return numeral(value).format("0,0.00"); 8 | }; 9 | const intFormat = (value: number): string => { 10 | return numeral(value).format("0,0"); 11 | }; 12 | const pad = (num: number, size: number) => { 13 | const s = "000000000" + num; 14 | return s.substring(s.length - size); 15 | }; 16 | 17 | export default { 18 | numberFormat, 19 | decimalFormat, 20 | intFormat, 21 | pad, 22 | }; 23 | -------------------------------------------------------------------------------- /app/components/ui/badges/ColorBadge.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Colors } from "~/application/enums/shared/Colors"; 3 | import { getBadgeColor } from "~/utils/shared/ColorUtils"; 4 | 5 | interface Props { 6 | color: Colors; 7 | } 8 | 9 | export default function ColorBadge({ color }: Props) { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/components/ui/icons/EyeIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function EyeIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/utils/shared/ObjectUtils.ts: -------------------------------------------------------------------------------- 1 | export function updateItem(items: any[], setItems: any, id: string, itemAttributes: any, idProp = "id") { 2 | const index = items.findIndex((x) => x[idProp] === id); 3 | if (index !== -1) { 4 | setItems([...items.slice(0, index), Object.assign({}, items[index], itemAttributes), ...items.slice(index + 1)]); 5 | } 6 | } 7 | 8 | export function updateItemByIdx(items: any[], setItems: any, index: number, itemAttributes: any) { 9 | if (index !== -1) { 10 | setItems([...items.slice(0, index), Object.assign({}, items[index], itemAttributes), ...items.slice(index + 1)]); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/routes/401.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import Page401 from "~/components/pages/Page401"; 3 | import { i18nHelper } from "~/locale/i18n.utils"; 4 | 5 | export let loader: LoaderFunction = async ({ request }) => { 6 | let { t, translations } = await i18nHelper(request); 7 | return json({ 8 | title: `${t("shared.unauthorized")} | ${process.env.APP_NAME}`, 9 | i18n: translations, 10 | }); 11 | }; 12 | 13 | export const meta: MetaFunction = ({ data }) => ({ 14 | title: data?.title, 15 | }); 16 | 17 | export default function Route401() { 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ui/transitions/EaseInAndOut.tsx: -------------------------------------------------------------------------------- 1 | import { Transition } from "@headlessui/react"; 2 | import { ReactNode } from "react"; 3 | 4 | interface Props { 5 | show: boolean; 6 | children: ReactNode; 7 | } 8 | export default function EaseInAndOut({ show, children }: Props) { 9 | return ( 10 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/application/dtos/subscriptions/SubscriptionProductDto.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionPriceDto } from "./SubscriptionPriceDto"; 2 | import { SubscriptionFeatureDto } from "./SubscriptionFeatureDto"; 3 | import { PricingModel } from "~/application/enums/subscriptions/PricingModel"; 4 | 5 | export interface SubscriptionProductDto { 6 | id?: string; 7 | stripeId: string; 8 | order: number; 9 | title: string; 10 | description: string; 11 | badge: string; 12 | active: boolean; 13 | model: PricingModel; 14 | public: boolean; 15 | prices: SubscriptionPriceDto[]; 16 | features: SubscriptionFeatureDto[]; 17 | translatedTitle?: string; 18 | } 19 | -------------------------------------------------------------------------------- /app/components/ui/icons/RefreshIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function RefreshIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/datepickers/DateInputButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from "react"; 2 | import { format } from "date-fns"; 3 | 4 | interface Props { 5 | value?: any; 6 | onClick?: () => void; 7 | } 8 | const DateInputButton = ({ value, onClick }: Props, ref) => ( 9 | 17 | ); 18 | 19 | export default forwardRef(DateInputButton); 20 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/selector/PreviewInputSelector.tsx: -------------------------------------------------------------------------------- 1 | import InputSelector from "~/components/ui/input/InputSelector"; 2 | 3 | export default function PreviewInputSelector() { 4 | return ( 5 |
6 |
7 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/front/Icon.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Link } from "@remix-run/react"; 3 | import IconLight from "~/assets/img/icon-light.png"; 4 | import IconDark from "~/assets/img/icon-dark.png"; 5 | 6 | interface Props { 7 | className?: string; 8 | size?: string; 9 | } 10 | 11 | export default function Icon({ className = "", size = "h-9" }: Props) { 12 | return ( 13 | 14 | {/* */} 15 | Logo 16 | Logo 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/application/dtos/entities/RowValueDto.ts: -------------------------------------------------------------------------------- 1 | import { PropertyOption, Media } from "@prisma/client"; 2 | import { PropertyWithDetails } from "~/utils/db/entities/entities.db.server"; 3 | import { RowWithDetails } from "~/utils/db/entities/rows.db.server"; 4 | 5 | export type RowValueDto = { 6 | id?: string | null; 7 | property: PropertyWithDetails; 8 | propertyId: string; 9 | relatedRowId?: string | undefined; 10 | idValue?: string | undefined; 11 | textValue?: string | undefined; 12 | numberValue?: number | undefined; 13 | dateValue?: Date | undefined; 14 | booleanValue?: boolean | undefined; 15 | selectedOption?: PropertyOption | undefined; 16 | relatedRow?: RowWithDetails | undefined; 17 | media?: Media[]; 18 | }; 19 | -------------------------------------------------------------------------------- /app/components/front/Logo.tsx: -------------------------------------------------------------------------------- 1 | import LogoLight from "~/assets/img/logo-light.png"; 2 | import LogoDark from "~/assets/img/logo-dark.png"; 3 | import clsx from "clsx"; 4 | import { Link } from "@remix-run/react"; 5 | 6 | interface Props { 7 | className?: string; 8 | size?: string; 9 | } 10 | 11 | export default function Logo({ className = "", size = "h-9" }: Props) { 12 | return ( 13 | 14 | {/* */} 15 | Logo 16 | Logo 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/routes/components/logo-and-icon.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Logo and Icon | Components | RemixBlocks - Your brand logo 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 9 | 10 | 19 | 20 | # Logo and Icon 21 | 22 | ## Logo 23 | 24 | import PreviewLogo from "~/components/ui/logo-and-icon/PreviewLogo"; 25 | 26 | 27 | 28 | ## Icon 29 | 30 | import PreviewIcon from "~/components/ui/logo-and-icon/PreviewIcon"; 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/routes/components/pdf-viewer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: PDF Viewer | Components | RemixBlocks - Preview PDF files 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 9 | 10 | 19 | 20 | # PDF Viewer 21 | 22 | import PreviewPdfViewers from "~/components/ui/pdf/PreviewPdfViewers"; 23 | import PdfViewer from "~/components/ui/pdf/PdfViewer"; 24 | 25 | 26 | 27 | ```tsx 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /app/routes/components/inputs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Inputs | Components | RemixBlocks - Text, Number, Date... 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 9 | 10 | 19 | 20 | # Input Components 21 | 22 | - [Text](/components/inputs/text) 23 | - [Number](/components/inputs/number) 24 | - [Date](/components/inputs/date) 25 | - [Selector](/components/inputs/selector) 26 | - [Checkbox](/components/inputs/checkbox) 27 | - [RadioGroup](/components/inputs/radio-group) 28 | -------------------------------------------------------------------------------- /app/components/ui/tabs/PreviewTabsAsButtons.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Tabs from "./Tabs"; 3 | 4 | export default function PreviewTabsAsButtons() { 5 | const [selectedTab, setSelectedTab] = useState(0); 6 | 7 | return ( 8 |
9 | { 12 | setSelectedTab(selected); 13 | }} 14 | className="w-full sm:w-auto" 15 | tabs={[{ name: "Tab 1" }, { name: "Tab 2" }, { name: "Tab 3" }]} 16 | /> 17 |
18 | {selectedTab === 0 ?
Tab 1 Content...
: selectedTab === 1 ?
Tab 2 Content...
:
Tab 3 Content...
} 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/components/ui/tabs/PreviewTabs.tsx: -------------------------------------------------------------------------------- 1 | import PreviewTabsAsButtons from "./PreviewTabsAsButtons"; 2 | import PreviewTabsAsLinks from "./PreviewTabsAsLinks"; 3 | 4 | export default function PreviewTabs() { 5 | return ( 6 |
7 |
8 |

Tab - as Links

9 |
10 | 11 |
12 |
13 | 14 |
15 |

Tab - as Buttons

16 |
17 | 18 |
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/selector/PreviewInputSelect.tsx: -------------------------------------------------------------------------------- 1 | import InputSelect from "~/components/ui/input/InputSelect"; 2 | 3 | export default function PreviewInputSelect() { 4 | return ( 5 |
6 |
7 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/text/PreviewInputTextWithTranslation.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputText from "~/components/ui/input/InputText"; 3 | 4 | export default function PreviewInputTextWithTranslation() { 5 | const [valid, setValid] = useState("shared.hi"); 6 | const [invalid, setInvalid] = useState("shared.invalid.i18n.key"); 7 | return ( 8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/components/ui/forms/InputGroup.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | interface Props { 4 | title: string; 5 | icon?: ReactNode; 6 | children: ReactNode; 7 | description?: string; 8 | } 9 | export default function InputGroup({ title, description, icon, children }: Props) { 10 | return ( 11 |
12 |

13 |
14 | {icon} 15 |
16 | {title} 17 |
18 |
19 |

20 |

{description}

21 |
{children}
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/ui/loaders/PreviewFloatingLoader.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import ButtonPrimary from "../buttons/ButtonPrimary"; 3 | import FloatingLoader from "./FloatingLoader"; 4 | 5 | export default function PreviewFloatingLoader() { 6 | const [open, setOpen] = useState(false); 7 | return ( 8 |
9 |
10 |
11 | setOpen(!open)}>{open ? "Hide floating loader" : "Show floating loader"} 12 | 13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import i18next from "i18next"; 2 | import { hydrate } from "react-dom"; 3 | import { I18nextProvider, initReactI18next } from "react-i18next"; 4 | import { RemixBrowser } from "@remix-run/react"; 5 | 6 | i18next 7 | .use(initReactI18next) 8 | .init({ 9 | supportedLngs: ["es", "en"], 10 | defaultNS: "translations", 11 | fallbackLng: "en", 12 | // I recommend you to always disable react.useSuspense for i18next 13 | react: { useSuspense: false }, 14 | detection: { 15 | caches: ["cookie"], 16 | lookupCookie: "lng", 17 | }, 18 | }) 19 | .then(() => { 20 | // then hydrate your app wrapped in the RemixI18NextProvider 21 | return hydrate( 22 | 23 | 24 | , 25 | document 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/radio/PreviewInputRadioGroup.tsx: -------------------------------------------------------------------------------- 1 | import InputRadioGroup from "~/components/ui/input/InputRadioGroup"; 2 | 3 | export default function PreviewInputRadioGroup() { 4 | return ( 5 |
6 |
7 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/application/dtos/subscriptions/SubscriptionPriceDto.ts: -------------------------------------------------------------------------------- 1 | import { SubscriptionProductDto } from "./SubscriptionProductDto"; 2 | import { SubscriptionPriceType } from "~/application/enums/subscriptions/SubscriptionPriceType"; 3 | import { SubscriptionBillingPeriod } from "~/application/enums/subscriptions/SubscriptionBillingPeriod"; 4 | import { Tenant, TenantSubscription } from ".prisma/client"; 5 | 6 | export interface SubscriptionPriceDto { 7 | id?: string; 8 | stripeId: string; 9 | type: SubscriptionPriceType; 10 | billingPeriod: SubscriptionBillingPeriod; 11 | price: number; 12 | currency: string; 13 | trialDays: number; 14 | active: boolean; 15 | priceBefore?: number; 16 | subscriptionProductId: string; 17 | subscriptionProduct?: SubscriptionProductDto; 18 | tenantSubscriptions?: (TenantSubscription & { tenant: Tenant })[]; 19 | } 20 | -------------------------------------------------------------------------------- /app/components/ui/dropdowns/PreviewDropdowns.tsx: -------------------------------------------------------------------------------- 1 | import PreviewDropdownsSimple from "./PreviewDropdownsSimple"; 2 | import PreviewDropdownsWithClick from "./PreviewDropdownsWithClick"; 3 | 4 | export default function PreviewDropdowns() { 5 | return ( 6 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/routes/blocks/forms/simple-form.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/blocks/lists/table.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/components/badges.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Badges | Components | RemixBlocks - Colorful badges 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 9 | 10 | 19 | 20 | # Badges 21 | 22 | import PreviewColorBadges from "~/components/ui/badges/PreviewColorBadges"; 23 | import PreviewSimpleBadges from "~/components/ui/badges/PreviewSimpleBadges"; 24 | 25 | ### ColorBadge 26 | 27 | 28 | 29 | ```tsx 30 | 31 | ``` 32 | 33 | ### SimpleBadge 34 | 35 | 36 | 37 | ```tsx 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /app/routes/blocks/email/newsletter-with-convertkit.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/blocks/forms/form-with-all-input-types.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/components/ui/loaders/Loading.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { useTransition } from "@remix-run/react"; 3 | 4 | interface Props { 5 | small?: boolean; 6 | loading?: boolean; 7 | } 8 | export default function Loading({ small = false, loading }: Props) { 9 | const transition = useTransition(); 10 | return ( 11 | <> 12 | {(transition.state === "submitting" || transition.state === "loading" || loading) && ( 13 |
14 |
15 |
18 |
19 |
20 | )} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/routes/blocks/email/contact-form-with-formspree.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/blocks/forms/form-with-confirmation-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /app/routes/blocks/forms/multiple-forms-on-one-route.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/blocks/subscriptions/create-pricing-plans-with-stripe.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | 7 | export const links = () => { 8 | return [{ rel: "stylesheet", href: styles }]; 9 | }; 10 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 11 | export let loader: LoaderFunction = async ({ request }) => { 12 | return json(await getBlockLoaderData({ request })); 13 | }; 14 | export default function Example() { 15 | const data = useLoaderData(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev').AppConfig} 3 | */ 4 | module.exports = { 5 | serverDependenciesToBundle: ["marked"], 6 | // serverBuildTarget: "vercel", 7 | // When running locally in development mode, we use the built in remix 8 | // server. This does not understand the vercel lambda module format, 9 | // so we default back to the standard build output. 10 | // server: process.env.NODE_ENV === "development" ? undefined : "./server.js", 11 | ignoredRouteFiles: [".*"], 12 | // appDirectory: "app", 13 | // assetsBuildDirectory: "public/build", 14 | // serverBuildPath: "api/index.js", 15 | // publicPath: "/build/", 16 | // devServerPort: 8002 17 | mdx: async (filename) => { 18 | const [rehypeHighlight] = await Promise.all([import("rehype-highlight").then((mod) => mod.default)]); 19 | return { 20 | rehypePlugins: [rehypeHighlight], 21 | }; 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /app/components/ui/icons/GitHubIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function GitHubIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/selector/PreviewInputSelectWithState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputSelect from "~/components/ui/input/InputSelect"; 3 | 4 | type SelectType = string | number | undefined; 5 | export default function PreviewInputSelectWithState() { 6 | const [value, setValue] = useState("2"); 7 | return ( 8 |
9 |
10 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/routes/components/modals.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Modals | Components | RemixBlocks 4 | description: RemixBlocks Modal Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Modals 22 | 23 | import PreviewModals from "~/components/ui/modals/PreviewModals"; 24 | 25 | 26 | 27 | ```tsx 28 | alert("Closed success modal")} /> 29 | alert("Closed error modal")} /> 30 | alert("No")} onYes={onYes} /> 31 | Content here 32 | ``` 33 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/selector/PreviewInputSelectorWithState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputSelector from "~/components/ui/input/InputSelector"; 3 | 4 | type SelectType = string | number | undefined; 5 | export default function PreviewInputSelectorWithState() { 6 | const [value, setValue] = useState("2"); 7 | return ( 8 |
9 |
10 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/components/ui/layouts/NewPageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import BreadcrumbSimple from "../breadcrumbs/BreadcrumbSimple"; 3 | 4 | interface Props { 5 | title: string; 6 | menu?: { 7 | title: string; 8 | routePath?: string; 9 | }[]; 10 | buttons?: ReactNode; 11 | children: ReactNode; 12 | } 13 | export default function NewPageLayout({ title, menu, buttons, children }: Props) { 14 | return ( 15 |
16 |
17 |
18 |

{title}

19 |
{buttons}
20 |
21 | 22 | {menu && } 23 |
24 | 25 | {children} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/components/blocks/BlockLoader.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "@remix-run/node"; 2 | import { Language } from "remix-i18next"; 3 | import { i18nHelper } from "~/locale/i18n.utils"; 4 | import UrlUtils from "~/utils/shared/UrlUtils"; 5 | import { BlockItem, getBlockByPath } from "./BlockItems"; 6 | 7 | export type BlockLoaderData = { 8 | title: string; 9 | i18n: Record; 10 | block: BlockItem; 11 | }; 12 | 13 | export async function getBlockLoaderData({ request }: { request: Request }) { 14 | let { translations } = await i18nHelper(request); 15 | 16 | const pathname = UrlUtils.stripTrailingSlash(new URL(request.url).pathname); 17 | const block = getBlockByPath(pathname.replace("/code", "")); 18 | if (!block) { 19 | throw redirect("/404"); 20 | } 21 | const data: BlockLoaderData = { 22 | title: `${block.title} | ${process.env.APP_NAME}`, 23 | i18n: translations, 24 | block, 25 | }; 26 | return data; 27 | } 28 | -------------------------------------------------------------------------------- /app/components/ui/icons/VariableIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function VariableIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/ui/uploaders/PreviewUploadersDocument.tsx: -------------------------------------------------------------------------------- 1 | import { FileBase64 } from "~/application/dtos/shared/FileBase64"; 2 | import UploadDocuments from "./UploadDocument"; 3 | 4 | export default function PreviewUploadersDocument() { 5 | function droppedDocuments(fileBase64: FileBase64[], files: any[]) { 6 | alert(`@droppedDocuments ${files.length} files: ` + files.map((f) => f.base64.substr(0, 30) + "...")); 7 | } 8 | function droppedDocument(base64: string, file: File) { 9 | alert(`@droppedDocument: base64 [${base64.substr(0, 30)}...], file [name (${file.name}), size (${file.size})]`); 10 | } 11 | return ( 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/routes/components/loaders.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Loaders | Components | RemixBlocks - Loading animations 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 9 | 10 | 19 | 20 | # Loaders 21 | 22 | import PreviewLoaders from "~/components/ui/loaders/PreviewLoaders"; 23 | import Loading from "~/components/ui/loaders/Loading"; 24 | 25 | ### Loading 26 | 27 | 28 | 29 | ```tsx 30 | 31 | ``` 32 | 33 | import PreviewFloatingLoader from "~/components/ui/loaders/PreviewFloatingLoader"; 34 | import FloatingLoader from "~/components/ui/loaders/FloatingLoader"; 35 | 36 | ### FloatingLoader 37 | 38 | 39 | 40 | ```tsx 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /app/components/ui/banners/PreviewBanners.tsx: -------------------------------------------------------------------------------- 1 | import InfoBanner from "./InfoBanner"; 2 | import WarningBanner from "./WarningBanner"; 3 | 4 | export default function PreviewBanners() { 5 | return ( 6 |
7 |

InfoBanner

8 | 9 | 10 |

Warning

11 | 12 | 13 |

Warning with Link

14 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/routes/components/uploaders.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Uploaders | Components | RemixBlocks 4 | description: RemixBlocks Upload File Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Uploaders 22 | 23 | import PreviewUploadersDocument from "~/components/ui/uploaders/PreviewUploadersDocument"; 24 | 25 | 26 | 27 | ```tsx 28 | import { FileBase64 } from "~/application/dtos/shared/FileBase64"; 29 | 30 | export default function PreviewUploaders() { 31 | function onDropped(base64: string, file: File) { 32 | alert(`@onDropped: [${base64.substr(0, 30)}...], [name (${file.name}), size (${file.size})]`); 33 | } 34 | return ; 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /app/components/ui/emptyState/PreviewEmptyStates.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "@remix-run/react"; 2 | import EmptyState from "./EmptyState"; 3 | 4 | export default function PreviewEmptyStates() { 5 | const currentRoute = useLocation().pathname; 6 | return ( 7 |
8 |
9 |
10 | alert("Clicked")} 12 | captions={{ 13 | new: "Button", 14 | thereAreNo: "There are no...", 15 | description: "Description...", 16 | }} 17 | icon="plus" 18 | /> 19 | 27 |
28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/components/ui/logo-and-icon/PreviewIcon.tsx: -------------------------------------------------------------------------------- 1 | import IconLight from "~/assets/img/icon-light.png"; 2 | import IconDark from "~/assets/img/icon-dark.png"; 3 | 4 | export default function PreviewIcon() { 5 | return ( 6 |
7 |
8 |
9 | Icon 10 |
11 |
12 | 13 |
14 |
15 | Icon 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/components/ui/logo-and-icon/PreviewLogo.tsx: -------------------------------------------------------------------------------- 1 | import LogoLight from "~/assets/img/logo-light.png"; 2 | import LogoDark from "~/assets/img/logo-dark.png"; 3 | 4 | export default function PreviewLogo() { 5 | return ( 6 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/components/ui/input/previews/radio/PreviewInputRadioGroupWithState.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import InputRadioGroup from "~/components/ui/input/InputRadioGroup"; 3 | 4 | type SelectType = string | number | undefined; 5 | export default function PreviewInputRadioGroupWithState() { 6 | const [value, setValue] = useState(2); 7 | return ( 8 |
9 |
10 | 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/routes/components/empty-states.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Empty States | Components | RemixBlocks 4 | description: RemixBlocks Empty States Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Empty State 22 | 23 | import EmptyState from "~/components/ui/emptyState/EmptyState"; 24 | 25 | alert("Clicked")} 28 | captions={{ 29 | new: "Button", 30 | thereAreNo: "There are no...", 31 | description: "Description...", 32 | }} 33 | icon="plus" 34 | /> 35 | 36 | ```tsx 37 | alert("Clicked")} 40 | captions={{ 41 | new: "Button", 42 | thereAreNo: "There are no...", 43 | description: "Description...", 44 | }} 45 | icon="plus" 46 | /> 47 | ``` 48 | -------------------------------------------------------------------------------- /app/utils/shared/CommandUtils.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "~/application/dtos/layout/Command"; 2 | import { SidebarItems } from "~/application/sidebar/SidebarItems"; 3 | import { SideBarItem } from "~/application/sidebar/SidebarItem"; 4 | 5 | function getCommands(): Command[] { 6 | const commands: Command[] = []; 7 | SidebarItems.forEach((item) => { 8 | commands.push(...getCommandsFromItem(item, [], [])); 9 | }); 10 | return commands; 11 | } 12 | 13 | function getCommandsFromItem(item: SideBarItem, commands: Command[], parent: string[]) { 14 | if (item.path && item.title && !item.hideFromCommandPalette) { 15 | let description = item.description ?? ""; 16 | if (parent.length > 0) { 17 | description = parent.join(" / "); 18 | } 19 | commands.push({ 20 | command: "", 21 | title: item.title, 22 | description, 23 | toPath: item.path, 24 | }); 25 | } 26 | item.items?.forEach((subItem) => { 27 | return getCommandsFromItem(subItem, commands, [...parent, item.title]); 28 | }); 29 | 30 | return commands; 31 | } 32 | 33 | export default { 34 | getCommands, 35 | }; 36 | -------------------------------------------------------------------------------- /app/components/ui/layouts/EditPageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { useParams } from "@remix-run/react"; 3 | import BreadcrumbSimple from "../breadcrumbs/BreadcrumbSimple"; 4 | 5 | interface Props { 6 | title: string; 7 | menu?: { 8 | title: string; 9 | routePath?: string; 10 | }[]; 11 | buttons?: ReactNode; 12 | children: ReactNode; 13 | } 14 | export default function EditPageLayout({ title, menu, buttons, children }: Props) { 15 | const params = useParams(); 16 | const home = params.tenant ? `/app/${params.tenant}/dashboard` : "/admin/dashboard"; 17 | return ( 18 |
19 |
20 |
21 |

{title}

22 |
{buttons}
23 |
24 | 25 | {menu && } 26 |
27 | 28 | {children} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/routes/blocks/email/send-email-with-postmark-template.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData, Outlet } from "@remix-run/react"; 3 | import BlockLayout from "~/components/blocks/BlockLayout"; 4 | import { BlockLoaderData, getBlockLoaderData } from "~/components/blocks/BlockLoader"; 5 | import styles from "highlight.js/styles/night-owl.css"; 6 | import InfoBanner from "~/components/ui/banners/InfoBanner"; 7 | 8 | export const links = () => { 9 | return [{ rel: "stylesheet", href: styles }]; 10 | }; 11 | export const meta: MetaFunction = ({ data }) => ({ title: data?.title }); 12 | export let loader: LoaderFunction = async ({ request }) => { 13 | return json(await getBlockLoaderData({ request })); 14 | }; 15 | export default function Example() { 16 | const data = useLoaderData(); 17 | return ( 18 | } 21 | > 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | MIT License 4 | 5 | Copyright (c) 2022 Alexandro Martínez 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /app/utils/shared/UrlUtils.ts: -------------------------------------------------------------------------------- 1 | import { Params } from "react-router"; 2 | 3 | const stripTrailingSlash = (str: string) => { 4 | return str.endsWith("/") ? str.slice(0, -1) : str; 5 | }; 6 | 7 | const currentTenantUrl = (params: Params, path?: string) => { 8 | const { tenant } = params; 9 | if (path) { 10 | const appPath = path.startsWith("/") ? path.substring(1, path.length - 1) : path; 11 | // console.log({ appPath }); 12 | return `/app/${tenant}/${appPath}`; 13 | } 14 | return `/app/${tenant}/`; 15 | }; 16 | 17 | const replaceVariables = (params: Params, path?: string) => { 18 | return path?.replace(":tenant", params.tenant ?? ""); 19 | }; 20 | 21 | const slugify = (str: string, max: number = 25) => { 22 | let value = str 23 | .toLowerCase() 24 | .trim() 25 | .replace("/", "-") 26 | .replace(/[^\w\s-]/g, "") 27 | .replace(/[\s_-]+/g, "-") 28 | .replace(/^-+|-+$/g, ""); 29 | if (max > 0) { 30 | value = value.padEnd(25, "").substring(0, 25); 31 | } 32 | return value.trim(); 33 | }; 34 | 35 | export default { 36 | currentTenantUrl, 37 | stripTrailingSlash, 38 | slugify, 39 | replaceVariables, 40 | }; 41 | -------------------------------------------------------------------------------- /app/components/app/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from "react"; 2 | import SidebarLayout from "../layouts/SidebarLayout"; 3 | import { Command } from "~/application/dtos/layout/Command"; 4 | import CommandPalette from "../ui/commandPalettes/CommandPalette"; 5 | 6 | interface Props { 7 | children: ReactNode; 8 | commands?: Command[]; 9 | } 10 | 11 | export default function AppLayout({ children, commands }: Props) { 12 | const [showCommandPalette, setShowCommandPalette] = useState(false); 13 | 14 | useEffect(() => { 15 | function onKeydown(event: any) { 16 | if (event.key === "k" && (event.metaKey || event.ctrlKey)) { 17 | setShowCommandPalette(true); 18 | } 19 | } 20 | window.addEventListener("keydown", onKeydown); 21 | return () => { 22 | window.removeEventListener("keydown", onKeydown); 23 | }; 24 | }, []); 25 | 26 | return ( 27 |
28 | setShowCommandPalette(true)}>{children} 29 | {commands && setShowCommandPalette(false)} commands={commands} />} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/components/ui/uploaders/UploadImage.tsx: -------------------------------------------------------------------------------- 1 | import SlideOver from "../slideOvers/SlideOver"; 2 | import { useState } from "react"; 3 | import UploadDocument from "./UploadDocument"; 4 | 5 | interface Props { 6 | title: string; 7 | initialImage?: string; 8 | onLoaded: (image: string, file: File) => void; 9 | onClose: () => void; 10 | } 11 | 12 | export default function UploadImage({ title = "", initialImage, onLoaded, onClose }: Props) { 13 | const [image, setImage] = useState(initialImage); 14 | 15 | function onChange(base64: string, file: File) { 16 | setImage(base64); 17 | onLoaded(base64, file); 18 | onClose(); 19 | } 20 | 21 | return ( 22 |
23 | 28 | 29 | {image && ( 30 |
31 | Uploaded 32 |
33 | )} 34 |
35 | } 36 | > 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/routes/components/command-palette.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Command Palette | Components | RemixBlocks 4 | description: RemixBlocks Command Palette Component 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Command Palettes 22 | 23 | import PreviewCommandPalettes from "~/components/ui/commandPalettes/PreviewCommandPalettes"; 24 | 25 | ### Command Palette 26 | 27 | 28 | 29 | ```tsx 30 | setOpen(false)} 33 | commands={[ 34 | { 35 | command: "1", 36 | title: "Title 1", 37 | description: "Description #1", 38 | onSelected: () => alert("Selected command #1"), 39 | }, 40 | { 41 | command: "2", 42 | title: "Title 2", 43 | description: "Description 2", 44 | onSelected: () => alert("Selected command #2"), 45 | }, 46 | ]} 47 | /> 48 | ``` 49 | -------------------------------------------------------------------------------- /app/components/ui/tooltips/HintTooltip.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | text: string; 3 | } 4 | 5 | export default function HintTooltip({ text }: Props) { 6 | return ( 7 |
8 | {text} 9 | {/* 10 | 15 | */} 16 | 17 | 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/utils/data/useRootData.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node"; 2 | import { Language } from "remix-i18next"; 3 | import { Command } from "~/application/dtos/layout/Command"; 4 | import { i18nHelper } from "~/locale/i18n.utils"; 5 | import { getUserInfo, UserSession } from "../session.server"; 6 | import CommandUtils from "../shared/CommandUtils"; 7 | import { useMatches } from "@remix-run/react"; 8 | 9 | export type AppRootData = { 10 | title: string; 11 | i18n: Record; 12 | userSession: UserSession; 13 | debug: boolean; 14 | commands: Command[]; 15 | }; 16 | 17 | export function useRootData(): AppRootData { 18 | return (useMatches().find((f) => f.pathname === "/" || f.pathname === "")?.data ?? {}) as AppRootData; 19 | } 20 | 21 | export async function loadRootData(request: Request) { 22 | let { translations } = await i18nHelper(request); 23 | const userSession = await getUserInfo(request); 24 | const commands = CommandUtils.getCommands(); 25 | const data: AppRootData = { 26 | title: `${process.env.APP_NAME}`, 27 | i18n: translations, 28 | userSession, 29 | debug: process.env.NODE_ENV === "development", 30 | commands, 31 | }; 32 | return json(data); 33 | } 34 | -------------------------------------------------------------------------------- /app/routes/components/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: UI Components | RemixBlocks Components - Buttons, Inputs... 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 9 | 10 | 11 | 12 | # Components 13 | 14 | - [Buttons](/components/buttons) 15 | - [Inputs](/components/inputs) 16 | - [Text](/components/inputs/text) 17 | - [Number](/components/inputs/number) 18 | - [Date](/components/inputs/date) 19 | - [Selector](/components/inputs/selector) 20 | - [Checkbox](/components/inputs/checkbox) 21 | - [RadioGroup](/components/inputs/radio-group) 22 | - [Badges](/components/badges) 23 | - [Banners](/components/banners) 24 | - [Breadcrumbs](/components/breadcrumbs) 25 | - [Command Palette](/components/command-palette) 26 | - [Dropdowns](/components/dropdowns) 27 | - [Empty States](/components/empty-states) 28 | - [Forms](/components/forms) 29 | - [Loaders](/components/loaders) 30 | - [Modals](/components/modals) 31 | - [PDF Viewer](/components/pdf-viewer) 32 | - [Tables](/components/tables) 33 | - [Tabs](/components/tabs) 34 | - [Tooltips](/components/tooltips) 35 | - [Uploaders](/components/uploaders) 36 | - [Logo and Icon](/components/logo-and-icon) 37 | -------------------------------------------------------------------------------- /app/components/ui/badges/PreviewSimpleBadges.tsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "~/application/enums/shared/Colors"; 2 | import SimpleBadge from "./SimpleBadge"; 3 | 4 | export default function PreviewBadges() { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/components/ui/layouts/IndexPageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import Tabs, { TabItem } from "../tabs/Tabs"; 3 | 4 | interface Props { 5 | title?: string; 6 | buttons?: ReactNode; 7 | children: ReactNode; 8 | tabs?: TabItem[]; 9 | } 10 | export default function IndexPageLayout({ title, buttons, children, tabs }: Props) { 11 | return ( 12 | <> 13 | {(title || buttons) && ( 14 |
15 |
16 |

{title}

17 | {buttons &&
{buttons}
} 18 |
19 |
20 | )} 21 | {tabs && ( 22 |
23 |
24 | 25 |
26 |
27 | )} 28 |
{children}
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/routes/components/buttons.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Buttons | Components | RemixBlocks 4 | description: RemixBlocks Button Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Buttons 22 | 23 | import PreviewButtons from "~/components/ui/buttons/PreviewButtons"; 24 | import PreviewButtonsAsLinks from "~/components/ui/buttons/PreviewButtonsAsLinks"; 25 | import PreviewButtonsDestructive from "~/components/ui/buttons/PreviewButtonsDestructive"; 26 | import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary"; 27 | 28 | ### Buttons 29 | 30 | 31 | 32 | ```tsx 33 | alert("Clicked")}>Primary 34 | ``` 35 | 36 | ### Buttons - as Links 37 | 38 | 39 | 40 | ```tsx 41 | Primary 42 | ``` 43 | 44 | ### Buttons - Destructive 45 | 46 | 47 | 48 | ```tsx 49 | alert("Clicked")}> 50 | Primary 51 | 52 | ``` 53 | -------------------------------------------------------------------------------- /app/routes/components/inputs/date.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Date Input | Components | RemixBlocks 4 | description: RemixBlocks Date Input Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 24 | 25 | # Date Input 26 | 27 | import PreviewInputDate from "~/components/ui/input/previews/date/PreviewInputDate"; 28 | 29 | ### InputDate 30 | 31 | 32 | 33 | ```tsx 34 | 35 | ``` 36 | 37 | import PreviewInputDateWithState from "~/components/ui/input/previews/date/PreviewInputDateWithState"; 38 | 39 | ### InputDate - with State 40 | 41 | 42 | 43 | ```tsx 44 | import { useState } from "react"; 45 | import InputDate from "~/components/ui/input/InputDate"; 46 | 47 | export default function PreviewInputDateWithState() { 48 | const [value, setValue] = useState(new Date("1990-01-02")); 49 | return setValue(e)} />; 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { createInstance } from "i18next"; 2 | import { renderToString } from "react-dom/server"; 3 | import { I18nextProvider, initReactI18next } from "react-i18next"; 4 | import { EntryContext } from "@remix-run/node"; 5 | import { RemixServer } from "@remix-run/react"; 6 | 7 | export default async function handleRequest(request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext) { 8 | // Here you also need to initialize i18next using initReactI18next, you should 9 | // use the same configuration as in your client side. 10 | // create an instance so every request will have a copy and don't re-use the 11 | // i18n object 12 | let i18n = createInstance(); 13 | await i18n.use(initReactI18next).init({ 14 | supportedLngs: ["es", "en"], 15 | defaultNS: "translations", 16 | react: { useSuspense: false }, 17 | }); 18 | 19 | // Then you can render your app wrapped in the RemixI18NextProvider as in the 20 | // entry.client file 21 | let markup = renderToString( 22 | 23 | 24 | 25 | ); 26 | 27 | responseHeaders.set("Content-Type", "text/html"); 28 | 29 | return new Response("" + markup, { 30 | status: responseStatusCode, 31 | headers: responseHeaders, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /app/components/ui/icons/OpenCloseArrowIcon.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | open: boolean; 3 | setOpen: React.Dispatch>; 4 | } 5 | export default function OpenCloseArrowIcon({ open, setOpen }: Props) { 6 | return ( 7 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/routes/components/forms.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Forms | Components | RemixBlocks 4 | description: RemixBlocks Form Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Forms 22 | 23 | ### InputGroup 24 | 25 | import InputGroup from "~/components/ui/forms/InputGroup"; 26 | import InputText from "~/components/ui/input/InputText"; 27 | 28 | 29 | 30 | 31 | 32 | ```tsx 33 | 34 | 35 | 36 | ``` 37 | 38 | import FormGroup from "~/components/ui/forms/FormGroup"; 39 | 40 | ### FormGroup 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ```tsx 49 | 50 | 51 | 52 | 53 | 54 | ``` 55 | -------------------------------------------------------------------------------- /app/components/ui/breadcrumbs/PreviewBreadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "@remix-run/react"; 2 | import Breadcrumb from "./Breadcrumb"; 3 | import BreadcrumbSimple from "./BreadcrumbSimple"; 4 | 5 | export default function PreviewBreadcrumbs() { 6 | const currentRoute = useLocation().pathname; 7 | return ( 8 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/routes/components/inputs/checkbox.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Checkbox Input | Components | RemixBlocks 4 | description: RemixBlocks Checkbox Input Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 24 | 25 | # Checkbox Input 26 | 27 | import PreviewInputCheckbox from "~/components/ui/input/previews/checkbox/PreviewInputCheckbox"; 28 | 29 | ### InputCheckbox 30 | 31 | 32 | 33 | ```tsx 34 | 35 | ``` 36 | 37 | import PreviewInputCheckboxWithState from "~/components/ui/input/previews/checkbox/PreviewInputCheckboxWithState"; 38 | 39 | ### InputCheckbox - with State 40 | 41 | 42 | 43 | ```tsx 44 | import { useState } from "react"; 45 | import InputCheckbox from "~/components/ui/input/InputCheckbox"; 46 | 47 | export default function PreviewInputCheckboxWithState() { 48 | const [value, setValue] = useState(true); 49 | return ; 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remix-blocks 2 | 3 | ![RemixBlocks](https://yahooder.sirv.com/remixblocks/seo/cover-2.png) 4 | 5 | ## What is RemixBlocks? 6 | 7 | Ready-to-use [Remix](https://remix.run) + [Tailwind CSS](https://tailwindcss.com/) routes, and [UI components](https://remixblocks.com/components), all blocks: 8 | 9 | - Are [full-stack](https://alexandro.dev/7-things-i-ve-learned-using-remix-for-1-month) routes. 10 | - Are independent of each other. 11 | - Have the code for you to copy-paste. 12 | - Can have a [loader](https://remix.run/docs/en/v1/guides/data-loading), [action](https://remix.run/docs/en/v1/guides/data-writes), [meta function](https://remix.run/docs/en/v1/api/conventions#meta), and a React component using [TypeScript](https://www.typescriptlang.org/) + [Tailwind CSS](https://tailwindcss.com/). 13 | 14 | ## Do you want to build your own SaaS? 15 | 16 | Check out [SaasRock](http://saasrock.com/?ref=remixblocks&utm_content=readme), a multi-tenant framework for building SaaS apps, all of these blocks are from SaasRock. 17 | 18 | ## Support 19 | 20 | If you like this project, [star it](https://github.com/AlexandroMtzG/remix-blocks) ⭐, or subscribe to [SaasRock](https://saasrock.com/?ref=remixblocks&utm_content=readme) for more 🚀. 21 | 22 | ## New blocks? 23 | 24 | Subscribe to the [newsletter](https://saasrock.com/newsletter?ref=remixblocks&utm_content=readme) or [follow me](https://twitter.com/AlexandroMtzG) on Twitter to get notified when new blocks are added. 25 | -------------------------------------------------------------------------------- /app/components/ui/commandPalettes/PreviewCommandPalettes.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import ButtonPrimary from "../buttons/ButtonPrimary"; 3 | import CommandPalette from "./CommandPalette"; 4 | 5 | export default function PreviewCommandPalettes() { 6 | const [open, setOpen] = useState(false); 7 | return ( 8 |
9 |
10 |
11 |
12 | setOpen(true)}>Show command palette 13 |
14 | setOpen(false)} 17 | commands={[ 18 | { 19 | command: "1", 20 | title: "Title 1", 21 | description: "Description #1", 22 | onSelected: () => alert("Selected command #1"), 23 | }, 24 | { 25 | command: "2", 26 | title: "Title 2", 27 | description: "Description 2", 28 | onSelected: () => alert("Selected command #2"), 29 | }, 30 | ]} 31 | /> 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/components/ui/dropdowns/DropdownOptions.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, ReactNode } from "react"; 2 | import { Menu, Transition } from "@headlessui/react"; 3 | import clsx from "clsx"; 4 | import OptionsIcon from "../icons/OptionsIcon"; 5 | 6 | interface Props { 7 | right?: boolean; 8 | options?: ReactNode; 9 | children?: ReactNode; 10 | className?: string; 11 | } 12 | 13 | export default function DropdownOptions({ options, right, className }: Props) { 14 | return ( 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | 31 | 37 |
{options}
38 |
39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/ui/buttons/LoadingButton.tsx: -------------------------------------------------------------------------------- 1 | import ButtonPrimary from "./ButtonPrimary"; 2 | import { forwardRef, MouseEventHandler, ReactNode, Ref, useImperativeHandle, useState } from "react"; 3 | import clsx from "clsx"; 4 | import { useTransition } from "@remix-run/react"; 5 | 6 | export interface RefLoadingButton { 7 | start: () => void; 8 | stop: () => void; 9 | } 10 | 11 | interface Props { 12 | className?: string; 13 | type?: "button" | "submit" | "reset" | undefined; 14 | disabled?: boolean; 15 | children: ReactNode; 16 | onClick?: MouseEventHandler; 17 | } 18 | 19 | const LoadingButton = ({ className, type = "button", children, disabled, onClick }: Props, ref: Ref) => { 20 | const transition = useTransition(); 21 | const [loading, setLoading] = useState(false); 22 | 23 | useImperativeHandle(ref, () => ({ 24 | start, 25 | stop, 26 | })); 27 | 28 | function start() { 29 | setLoading(true); 30 | } 31 | function stop() { 32 | setLoading(false); 33 | } 34 | const submitting = transition.state === "submitting"; 35 | 36 | return ( 37 | 43 | {children} 44 | 45 | ); 46 | }; 47 | 48 | export default forwardRef(LoadingButton); 49 | -------------------------------------------------------------------------------- /app/components/pages/Page404.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "@remix-run/react"; 2 | import Logo from "../front/Logo"; 3 | 4 | export default function Page404() { 5 | const navigate = useNavigate(); 6 | return ( 7 | <> 8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |

Not found

17 |

Page not found.

18 |

Sorry, we couldn’t find the page you’re looking for.

19 |
20 | 27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/components/pages/Page401.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "@remix-run/react"; 2 | import Logo from "../front/Logo"; 3 | 4 | export default function Page401() { 5 | const navigate = useNavigate(); 6 | return ( 7 | <> 8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |

Unauthorized

17 |

You're not authorized to see this page.

18 |

Contact your admin and verify your permissions.

19 |
20 | 27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/components/ui/buttons/PreviewButtonsAsLinks.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "@remix-run/react"; 2 | import ButtonPrimary from "./ButtonPrimary"; 3 | import ButtonSecondary from "./ButtonSecondary"; 4 | import ButtonTertiary from "./ButtonTertiary"; 5 | 6 | export default function PreviewButtonsAsLinks() { 7 | const currentRoute = useLocation().pathname; 8 | return ( 9 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/routes/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: RemixBlocks - Ready-to-use Remix + Tailwind CSS blocks 4 | description: RemixBlocks - Ready-to-use Remix and Tailwind CSS routes and UI components, all blocks are full-stack and independent to each other. 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | ## RemixBlocks 10 | 11 | Ready-to-use [Remix](https://remix.run) + [Tailwind CSS](https://tailwindcss.com/) routes, and [UI components](/components), all blocks: 12 | 13 | - Are [full-stack](https://alexandro.dev/7-things-i-ve-learned-using-remix-for-1-month) routes. 14 | - Are independent of each other. 15 | - Have the code for you to copy-paste. 16 | - Can have a [loader](https://remix.run/docs/en/v1/guides/data-loading), [action](https://remix.run/docs/en/v1/guides/data-writes), [meta function](https://remix.run/docs/en/v1/api/conventions#meta), and a React component using [TypeScript](https://www.typescriptlang.org/) + [Tailwind CSS](https://tailwindcss.com/). 17 | 18 | ## Do you want to build your own SaaS? 19 | 20 | Check out [SaasRock](http://saasrock.com/?ref=remixblocks&utm_content=index), a multi-tenant framework for building SaaS apps, all of these blocks are from SaasRock. 21 | 22 | ## Support 23 | 24 | If you like this project, [star it](https://github.com/AlexandroMtzG/remix-blocks) ⭐, or subscribe to [SaasRock](https://saasrock.com/?ref=remixblocks&utm_content=index) for more 🚀. 25 | 26 | ## New blocks? 27 | 28 | Subscribe to the [newsletter](https://saasrock.com/newsletter?ref=remixblocks&utm_content=index) or [follow me](https://twitter.com/AlexandroMtzG) on Twitter to get notified when new blocks are added. 29 | -------------------------------------------------------------------------------- /app/routes/components/inputs/radio-group.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Radio Group Input | Components | RemixBlocks 4 | description: RemixBlocks Radio Group Input Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 24 | 25 | # Radio Group Input 26 | 27 | import PreviewInputRadioGroupWithState from "~/components/ui/input/previews/radio/PreviewInputRadioGroupWithState"; 28 | 29 | ### InputRadioGroup 30 | 31 | 32 | 33 | ```tsx 34 | import { useState } from "react"; 35 | import InputRadioGroup from "~/components/ui/input/InputRadioGroup"; 36 | 37 | type SelectType = string | number | undefined; 38 | export default function PreviewInputRadioGroupWithState() { 39 | const [value, setValue] = useState(2); 40 | return ( 41 | 62 | ); 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /app/utils/shared/KeypressUtils.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | 3 | function useKeypress(callback, keyCode) { 4 | const escFunction = useCallback((event) => { 5 | if (event.keyCode === keyCode) { 6 | callback(); 7 | } 8 | // eslint-disable-next-line react-hooks/exhaustive-deps 9 | }, []); 10 | 11 | useEffect(() => { 12 | document.addEventListener("keydown", escFunction, false); 13 | 14 | return () => { 15 | document.removeEventListener("keydown", escFunction, false); 16 | }; 17 | // eslint-disable-next-line react-hooks/exhaustive-deps 18 | }, []); 19 | } 20 | 21 | export function useEscapeKeypress(callback) { 22 | useKeypress(callback, 27); 23 | } 24 | 25 | export function useEnterKeypress(callback) { 26 | useKeypress(callback, 13); 27 | } 28 | 29 | export function useOuterClick(callback) { 30 | const callbackRef = useRef(); // initialize mutable ref, which stores callback 31 | const innerRef = useRef(); // returned to client, who marks "border" element 32 | 33 | // update cb on each render, so second useEffect has access to current value 34 | useEffect(() => { 35 | callbackRef.current = callback; 36 | }); 37 | 38 | useEffect(() => { 39 | document.addEventListener("click", handleClick); 40 | return () => document.removeEventListener("click", handleClick); 41 | function handleClick(e) { 42 | if (innerRef.current && callbackRef.current && !innerRef.current.contains(e.target)) callbackRef.current(e); 43 | } 44 | }, []); // no dependencies -> stable click listener 45 | 46 | return innerRef; // convenience for client (doesn't need to init ref himself) 47 | } 48 | -------------------------------------------------------------------------------- /app/components/ui/input/InputCheckboxWithDescription.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { ReactNode } from "react"; 3 | import HintTooltip from "../tooltips/HintTooltip"; 4 | 5 | interface Props { 6 | name: string; 7 | title: string | ReactNode; 8 | description: string | ReactNode; 9 | value?: boolean; 10 | setValue?: React.Dispatch>; 11 | className?: string; 12 | help?: string; 13 | disabled?: boolean; 14 | } 15 | export default function InputCheckboxWithDescription({ name, title, value, setValue, description, className, help, disabled = false }: Props) { 16 | return ( 17 |
18 |
19 | 28 |
29 |
30 | { 37 | setValue?.(e.target.checked); 38 | }} 39 | disabled={disabled} 40 | className={clsx(disabled && "bg-gray-100 cursor-not-allowed", "cursor-pointer focus:ring-accent-500 h-4 w-4 text-accent-600 border-gray-300 rounded")} 41 | /> 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/components/ui/badges/PreviewColorBadges.tsx: -------------------------------------------------------------------------------- 1 | import { Colors } from "~/application/enums/shared/Colors"; 2 | import ColorBadge from "./ColorBadge"; 3 | 4 | export default function PreviewBadges() { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/routes/components/inputs/number.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Number Input | Components | RemixBlocks 4 | description: RemixBlocks Number Input Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 24 | 25 | # Number Input 26 | 27 | import PreviewInputNumber from "~/components/ui/input/previews/number/PreviewInputNumber"; 28 | 29 | ### InputNumber 30 | 31 | 32 | 33 | ```tsx 34 | 35 | ``` 36 | 37 | import PreviewInputNumberWithHintAndHelp from "~/components/ui/input/previews/number/PreviewInputNumberWithHintAndHelp"; 38 | 39 | ### InputNumber - with Hint and Help 40 | 41 | 42 | 43 | ```tsx 44 | Hint text} help={"Help text"} /> 45 | ``` 46 | 47 | import PreviewInputNumberWithAllOptions from "~/components/ui/input/previews/number/PreviewInputNumberWithAllOptions"; 48 | 49 | ### InputNumber - with all Options 50 | 51 | 52 | 53 | ```tsx 54 | import InputNumber from "~/components/ui/input/InputNumber"; 55 | 56 | export default function PreviewInputNumberWithAllOptions() { 57 | const [value, setValue] = useState(0); 58 | return ; 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /app/components/ui/buttons/PreviewButtonsDestructive.tsx: -------------------------------------------------------------------------------- 1 | import ButtonPrimary from "./ButtonPrimary"; 2 | import ButtonSecondary from "./ButtonSecondary"; 3 | import ButtonTertiary from "./ButtonTertiary"; 4 | 5 | export default function PreviewButtonsDestructive() { 6 | return ( 7 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/components/ui/input/InputSearch.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import ButtonPrimary from "../buttons/ButtonPrimary"; 3 | 4 | interface Props { 5 | value: string; 6 | setValue: React.Dispatch>; 7 | newTitle?: string; 8 | onNew?: () => void; 9 | onNewRoute?: string; 10 | } 11 | 12 | export default function InputSearch({ value, setValue, onNew, onNewRoute, newTitle }: Props) { 13 | const { t } = useTranslation(); 14 | return ( 15 |
16 |
17 |
18 | 19 | 24 | 25 |
26 | setValue(e.target.value)} 34 | autoComplete="off" 35 | /> 36 |
37 | {onNew && {newTitle ?? t("shared.new")}} 38 | {onNewRoute && {newTitle ?? t("shared.new")}} 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/routes/components/banners.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Banners | Components | RemixBlocks 4 | description: RemixBlocks Banner Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Banners 22 | 23 | import InfoBanner from "~/components/ui/banners/InfoBanner"; 24 | import WarningBanner from "~/components/ui/banners/WarningBanner"; 25 | 26 | ### InfoBanner 27 | 28 | 29 | 30 | ```tsx 31 | 32 | ``` 33 | 34 | ### WarningBanner 35 | 36 | 37 | 38 | ```tsx 39 | 40 | ``` 41 | 42 | ### WarningBanner - with Redirect 43 | 44 | 49 | 50 | ```tsx 51 | 56 | ``` 57 | -------------------------------------------------------------------------------- /app/components/ui/banners/InfoBanner.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Link } from "@remix-run/react"; 4 | 5 | interface Props { 6 | title: string; 7 | text: string; 8 | redirect?: string; 9 | children?: ReactNode; 10 | } 11 | 12 | export default function InfoBanner({ title = "Note", text = "", redirect, children }: Props) { 13 | const { t } = useTranslation(); 14 | return ( 15 |
16 |
17 |
18 |
19 | 20 | 25 | 26 |
27 | 28 |
29 |

{title}

30 |
31 |
32 | {text}{" "} 33 | {redirect && ( 34 | 35 | {t("shared.goTo")} {redirect} 36 | 37 | )} 38 | {children} 39 |
40 |
41 |
42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/ui/banners/WarningBanner.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { Link } from "@remix-run/react"; 4 | 5 | interface Props { 6 | title: string; 7 | text: string; 8 | redirect?: string; 9 | children?: ReactNode; 10 | } 11 | 12 | export default function WarningBanner({ title = "Warning", text = "", redirect, children }: Props) { 13 | const { t } = useTranslation(); 14 | return ( 15 |
16 |
17 |
18 |
19 | 20 | 25 | 26 |
27 | 28 |
29 |

{title}

30 |
31 |

32 | {text}{" "} 33 | {redirect && ( 34 | 35 | {t("shared.goTo")} {redirect} 36 | 37 | )} 38 | {children} 39 |

40 |
41 |
42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/ui/input/InputSelect.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "~/utils/shared/ClassesUtils"; 2 | 3 | interface Props { 4 | name: string; 5 | title: string; 6 | withLabel?: boolean; 7 | options: { name: string; value: string | number | undefined; disabled?: boolean }[]; 8 | value?: string | number | undefined; 9 | setValue?: React.Dispatch>; 10 | className?: string; 11 | required?: boolean; 12 | disabled?: boolean; 13 | } 14 | export default function InputSelect({ name, title, withLabel = true, value, options, setValue, className, required, disabled }: Props) { 15 | return ( 16 |
17 | {withLabel && ( 18 | 24 | )} 25 |
26 | 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/components/ui/images/LogoClouds.tsx: -------------------------------------------------------------------------------- 1 | import LogoReact from "~/assets/logos/react.png"; 2 | import LogoTypescript from "~/assets/logos/typescript.png"; 3 | import LogoTailwind from "~/assets/logos/tailwindcss.png"; 4 | import LogoPrisma from "~/assets/logos/prisma.png"; 5 | import LogoStripe from "~/assets/logos/stripe.png"; 6 | import LogoPostmark from "~/assets/logos/postmark.png"; 7 | import LogoRemix from "~/assets/logos/remix.png"; 8 | 9 | export default function LogoClouds() { 10 | return ( 11 |
12 |
13 |
14 |
15 | React 16 |
17 |
18 | TypeScript 19 |
20 |
21 | Tailwind CSS 22 |
23 |
24 | Remix 25 |
26 |
27 | Prisma 28 |
29 |
30 | Stripe 31 |
32 |
33 | Postmark 34 |
35 |
36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /styles/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .tooltip { 7 | @apply invisible absolute; 8 | } 9 | 10 | .has-tooltip:hover .tooltip { 11 | @apply visible z-50; 12 | } 13 | 14 | @keyframes spinner { 15 | to { 16 | transform: rotate(360deg); 17 | } 18 | } 19 | 20 | .base-spinner { 21 | position: relative; 22 | overflow: hidden; 23 | } 24 | 25 | .base-spinner:before { 26 | content: ""; 27 | box-sizing: border-box; 28 | position: absolute; 29 | background-color: inherit; 30 | width: 100%; 31 | height: 100%; 32 | display: block; 33 | z-index: 1; 34 | top: 0; 35 | left: 0; 36 | } 37 | 38 | .base-spinner:after { 39 | content: ""; 40 | box-sizing: border-box; 41 | position: absolute; 42 | top: 50%; 43 | left: 50%; 44 | width: 20px; 45 | height: 20px; 46 | margin-top: -10px; 47 | margin-left: -10px; 48 | border-radius: 50%; 49 | border: 2px solid rgba(255, 255, 255, 0.45); 50 | border-top-color: inherit; 51 | animation: spinner 0.6s linear infinite; 52 | z-index: 2; 53 | } 54 | 55 | .loader { 56 | border-top-color: #5a67d8; 57 | -webkit-animation: spinner 1.5s linear infinite; 58 | animation: spinner 1.5s linear infinite; 59 | } 60 | 61 | @-webkit-keyframes spinner { 62 | 0% { 63 | -webkit-transform: rotate(0deg); 64 | } 65 | 100% { 66 | -webkit-transform: rotate(360deg); 67 | } 68 | } 69 | 70 | @keyframes spinner { 71 | 0% { 72 | transform: rotate(0deg); 73 | } 74 | 100% { 75 | transform: rotate(360deg); 76 | } 77 | } 78 | 79 | .drop { 80 | align-items: center; 81 | justify-content: center; 82 | padding: 1rem; 83 | transition: background-color 0.2s ease-in-out; 84 | } 85 | 86 | #uploadmyfile { 87 | display: none; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/components/ui/buttons/PreviewButtons.tsx: -------------------------------------------------------------------------------- 1 | import ButtonPrimary from "./ButtonPrimary"; 2 | import ButtonSecondary from "./ButtonSecondary"; 3 | import ButtonTertiary from "./ButtonTertiary"; 4 | import LoadingButton, { RefLoadingButton } from "./LoadingButton"; 5 | import { useRef } from "react"; 6 | 7 | export default function PreviewButtons() { 8 | const loadingButton = useRef(null); 9 | function startLoading() { 10 | if (loadingButton.current) { 11 | loadingButton.current?.start(); 12 | setTimeout(() => { 13 | loadingButton.current?.stop(); 14 | }, 2000); 15 | } 16 | } 17 | return ( 18 |
19 |
20 |
21 |
22 | alert("Clicked primary button")}>Primary 23 | alert("Clicked secondary button")}>Secondary 24 | alert("Clicked tertiary button")}>Tertiary 25 | startLoading()}> 26 | Loading 27 | 28 |
29 |
30 | Primary 31 | Secondary 32 | Tertiary 33 | Loading 34 |
35 |
36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/components/ui/tables/CollapsibleRow.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from "@headlessui/react"; 2 | import clsx from "clsx"; 3 | import { ReactNode, useState } from "react"; 4 | import { useTranslation } from "react-i18next"; 5 | import DropdownOptions from "../dropdowns/DropdownOptions"; 6 | import OpenCloseArrowIcon from "../icons/OpenCloseArrowIcon"; 7 | 8 | export interface RefSimpleRow {} 9 | 10 | interface Props { 11 | value: ReactNode; 12 | title: string; 13 | children: ReactNode; 14 | onRemove: () => void; 15 | initial?: boolean; 16 | } 17 | 18 | export default function CollapsibleRow({ value, title, children, onRemove, initial = false }: Props) { 19 | const { t } = useTranslation(); 20 | const [open, setOpen] = useState(initial); 21 | 22 | return ( 23 |
24 |
25 | 28 |
29 | 32 | 33 | {({ active }) => ( 34 | 41 | )} 42 | 43 |
44 | } 45 | > 46 | 47 |
48 |
49 | {open &&
{children}
} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/routes/components/tabs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Tabs | Components | RemixBlocks 4 | description: RemixBlocks Tabs Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Tabs 22 | 23 | import PreviewTabsAsLinks from "~/components/ui/tabs/PreviewTabsAsLinks"; 24 | 25 | ### Tabs - as Links 26 | 27 | 28 | 29 | ```tsx 30 | 41 | ``` 42 | 43 | import PreviewTabsAsButtons from "~/components/ui/tabs/PreviewTabsAsButtons"; 44 | 45 | ### Tabs - as Buttons 46 | 47 | 48 | 49 | ```tsx 50 |
51 | { 54 | setSelectedTab(selected); 55 | }} 56 | className="w-full sm:w-auto" 57 | tabs={[{ name: "Tab 1" }, { name: "Tab 2" }, { name: "Tab 3" }]} 58 | /> 59 |
60 | {selectedTab === 0 ?
Tab 1 Content...
: selectedTab === 1 ?
Tab 2 Content...
:
Tab 3 Content...
} 61 |
62 |
63 | ``` 64 | 65 | import PreviewTabsVertical from "~/components/ui/tabs/PreviewTabsVertical"; 66 | 67 | ### Tabs - Vertical 68 | 69 | 70 | 71 | ```tsx 72 | 83 | ``` 84 | -------------------------------------------------------------------------------- /app/routes/components/breadcrumbs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Breadcrumbs | Components | RemixBlocks 4 | description: RemixBlocks Breadcrumb Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Breadcrumbs 22 | 23 | ### Breadcrumb 24 | 25 | import Breadcrumb from "~/components/ui/breadcrumbs/Breadcrumb"; 26 | 27 | 39 | 40 | ```tsx 41 | 53 | ``` 54 | 55 | ### BreadcrumbSimple 56 | 57 | import BreadcrumbSimple from "~/components/ui/breadcrumbs/BreadcrumbSimple"; 58 | 59 | 71 | 72 | ```tsx 73 | 85 | ``` 86 | -------------------------------------------------------------------------------- /app/routes/blocks/lists/table/index.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 2 | import { useLoaderData } from "@remix-run/react"; 3 | import TableSimple from "~/components/ui/tables/TableSimple"; 4 | 5 | interface EmployeeDto { 6 | id: number; 7 | firstName: string; 8 | lastName: string; 9 | email: string; 10 | phone: string; 11 | } 12 | 13 | type LoaderData = { 14 | title: string; 15 | items: EmployeeDto[]; 16 | }; 17 | export let loader: LoaderFunction = async () => { 18 | const items: EmployeeDto[] = [ 19 | { 20 | id: 1, 21 | firstName: "John", 22 | lastName: "Doe", 23 | email: "john.doe@company.com", 24 | phone: "555-555-5555", 25 | }, 26 | { 27 | id: 2, 28 | firstName: "Jane", 29 | lastName: "Deo", 30 | email: "jane.deo@company.com", 31 | phone: "777-777-777", 32 | }, 33 | ]; 34 | 35 | const data: LoaderData = { 36 | title: "Table | RemixBlocks", 37 | items, 38 | }; 39 | return json(data); 40 | }; 41 | 42 | export const meta: MetaFunction = ({ data }) => ({ 43 | title: data?.title, 44 | }); 45 | 46 | export default function Example() { 47 | const data = useLoaderData(); 48 | return ( 49 | alert(`${i.firstName} ${i.lastName}`), 55 | }, 56 | ]} 57 | headers={[ 58 | { 59 | name: "firstName", 60 | title: "First Name", 61 | value: (i) => i.firstName, 62 | }, 63 | { 64 | name: "lastName", 65 | title: "Last Name", 66 | value: (i) => i.lastName, 67 | }, 68 | { 69 | name: "email", 70 | title: "Email", 71 | value: (i) => i.email, 72 | className: "w-full", 73 | }, 74 | { 75 | name: "phone", 76 | title: "Phone", 77 | value: (i) => i.phone, 78 | breakpoint: "xl", 79 | }, 80 | ]} 81 | /> 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /app/components/ui/selectors/LayoutSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from "@headlessui/react"; 2 | import { ApplicationLayout } from "~/application/enums/shared/ApplicationLayout"; 3 | import clsx from "clsx"; 4 | import Dropdown from "../dropdowns/Dropdown"; 5 | import { useTranslation } from "react-i18next"; 6 | 7 | interface Props { 8 | className?: string; 9 | btnClassName?: string; 10 | } 11 | 12 | export default function LayoutSelector({ className, btnClassName }: Props) { 13 | const { t } = useTranslation(); 14 | 15 | const layouts = [ 16 | { 17 | name: t("shared.layouts.sidebar"), 18 | value: ApplicationLayout.SIDEBAR, 19 | }, 20 | { 21 | name: t("shared.layouts.stacked"), 22 | value: ApplicationLayout.STACKED, 23 | }, 24 | ]; 25 | 26 | function select(value: ApplicationLayout) { 27 | // store.dispatch(setLayout(value)); 28 | } 29 | 30 | return ( 31 | {t("settings.preferences.layouts")}} 38 | options={ 39 |
40 | {layouts.map((layout, index) => { 41 | return ( 42 | 43 | {({ active }) => ( 44 | 53 | )} 54 | 55 | ); 56 | })} 57 |
58 | } 59 | /> 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /app/components/ui/input/InputDate.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { forwardRef, Ref, RefObject, useImperativeHandle, useRef } from "react"; 3 | import HintTooltip from "~/components/ui/tooltips/HintTooltip"; 4 | 5 | export interface RefInputDate { 6 | input: RefObject; 7 | } 8 | 9 | interface Props { 10 | name: string; 11 | title: string; 12 | value?: Date; 13 | onChange?: (date: Date) => void; 14 | className?: string; 15 | help?: string; 16 | disabled?: boolean; 17 | readOnly?: boolean; 18 | required?: boolean; 19 | } 20 | const InputDate = ({ name, title, value, onChange, className, help, disabled = false, readOnly = false, required = false }: Props, ref: Ref) => { 21 | useImperativeHandle(ref, () => ({ input })); 22 | const input = useRef(null); 23 | 24 | return ( 25 |
26 | 36 |
37 | (onChange ? onChange(e.target.valueAsDate || new Date()) : {})} 45 | disabled={disabled} 46 | readOnly={readOnly} 47 | className={clsx( 48 | "w-full flex-1 focus:ring-accent-500 focus:border-accent-500 block min-w-0 rounded-md sm:text-sm border-gray-300", 49 | className, 50 | (disabled || readOnly) && "bg-gray-100 cursor-not-allowed" 51 | )} 52 | /> 53 |
54 |
55 | ); 56 | }; 57 | export default forwardRef(InputDate); 58 | -------------------------------------------------------------------------------- /app/components/ui/tables/PreviewTableSimple.tsx: -------------------------------------------------------------------------------- 1 | import DateUtils from "~/utils/shared/DateUtils"; 2 | import NumberUtils from "~/utils/shared/NumberUtils"; 3 | import TableSimple from "./TableSimple"; 4 | 5 | const items = [ 6 | { 7 | firstName: "John", 8 | lastName: "Doe", 9 | email: "john.doe@company.com", 10 | salary: 100, 11 | birthday: new Date(1990, 1, 1), 12 | }, 13 | { 14 | firstName: "Luna", 15 | lastName: "Davis", 16 | email: "luna.davis@company.com", 17 | salary: 100.5, 18 | birthday: new Date(1990, 12, 31), 19 | }, 20 | ]; 21 | export default function PreviewTableSimple() { 22 | return ( 23 |
24 |
25 |
26 | `${item.firstName} ${item.lastName}`, 33 | }, 34 | { 35 | title: "Email", 36 | name: "email", 37 | value: (item) => item.email, 38 | }, 39 | { 40 | title: "Salary", 41 | name: "salary", 42 | className: "text-blue-500", 43 | value: (item) => item.salary, 44 | formattedValue: (item) => NumberUtils.decimalFormat(item.salary), 45 | }, 46 | { 47 | title: "Birthday", 48 | name: "birthday", 49 | className: "text-gray-400", 50 | value: (item) => item.birthday, 51 | formattedValue: (item) => DateUtils.dateYMD(item.birthday), 52 | }, 53 | ]} 54 | actions={[ 55 | { 56 | title: "View", 57 | onClick: (_, item) => alert("Clicked: " + item.firstName), 58 | }, 59 | ]} 60 | > 61 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /app/utils/shared/DateUtils.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | const dateAgo = (value: Date | string | null | undefined): string => { 4 | const today = moment(new Date()); 5 | const at = moment(value); 6 | const days = Math.abs(today.diff(at, "days")); 7 | // if (unitOfTime) { 8 | // return moment(at) 9 | // .startOf(unitOfTime) 10 | // .fromNow(); 11 | // } 12 | if (days <= 1) { 13 | return moment(at).startOf("minute").fromNow(); 14 | } else if (days <= 7) { 15 | return moment(at).startOf("day").fromNow(); 16 | } else if (days <= 30) { 17 | return moment(at).startOf("week").fromNow(); 18 | } else if (days <= 30 * 12) { 19 | return moment(at).startOf("month").fromNow(); 20 | } else if (days <= 30 * 12 * 2) { 21 | return moment(at).startOf("year").fromNow(); 22 | } else { 23 | return moment(at).format("YYYY-MM-DD"); 24 | } 25 | }; 26 | const dateYMD = (value: Date | string | null | undefined): string => { 27 | return moment(value).format("YYYY-MM-DD"); 28 | }; 29 | const dateLL = (value: Date | string | null | undefined): string => { 30 | return moment(value).format("YYYY-MM-DD"); 31 | }; 32 | const dateYMDHMS = (value: Date | string | null | undefined): string => { 33 | return moment(value).format("YYYY-MM-DD HH:mm:ss"); 34 | }; 35 | const dateMonthName = (value: Date | string | null | undefined): string => { 36 | return moment(value).format("MMMM YYYY"); 37 | }; 38 | const dateDM = (value: Date | string | null | undefined): string => { 39 | return moment(value).format("D MMM"); 40 | }; 41 | const dateMonthDayYear = (value: Date | string | null | undefined): string => { 42 | return moment(value).format("MMMM D, YYYY"); 43 | }; 44 | const dateHMS = (value: Date | string | null | undefined): string => { 45 | return moment(value).format("HH:mm:ss"); 46 | }; 47 | 48 | const daysFromDate = (value: Date, days: number) => { 49 | return new Date(new Date().setDate(value.getDate() + days)); 50 | }; 51 | 52 | export default { 53 | dateAgo, 54 | dateYMD, 55 | dateLL, 56 | dateYMDHMS, 57 | dateMonthName, 58 | dateDM, 59 | dateHMS, 60 | dateMonthDayYear, 61 | daysFromDate, 62 | }; 63 | -------------------------------------------------------------------------------- /app/components/ui/media/PreviewMediaModal.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from "@headlessui/react"; 2 | import PdfViewer from "../pdf/PdfViewer"; 3 | import { MediaDto } from "~/application/dtos/entities/MediaDto"; 4 | import ButtonSecondary from "../buttons/ButtonSecondary"; 5 | import XIcon from "../icons/XIcon"; 6 | import OpenModal from "../modals/OpenModal"; 7 | import DownloadIcon from "../icons/DownloadIcon"; 8 | 9 | interface Props { 10 | item: MediaDto; 11 | onClose: () => void; 12 | onDownload: () => void; 13 | } 14 | 15 | export default function PreviewMediaModal({ item, onClose, onDownload }: Props) { 16 | function isPdf() { 17 | return item.type.endsWith("pdf"); 18 | } 19 | 20 | function isImage() { 21 | return item.type.includes("image"); 22 | } 23 | 24 | return ( 25 | 26 |
27 | {!item ? ( 28 |
Undefined
29 | ) : ( 30 |
31 |
32 | 33 | {item.title} 34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 | {isPdf() ? ( 46 |
47 | 48 |
49 | ) : isImage() ? ( 50 |
51 | {item.title} 52 |
53 | ) : ( 54 |
Not PDF
55 | )} 56 |
57 |
58 | )} 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /app/components/ui/breadcrumbs/BreadcrumbSimple.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { Link, useParams } from "@remix-run/react"; 3 | import UrlUtils from "~/utils/shared/UrlUtils"; 4 | import RightIcon from "../icons/RightIcon"; 5 | 6 | interface MenuItem { 7 | title: string; 8 | routePath?: string; 9 | } 10 | 11 | interface Props { 12 | menu: MenuItem[]; 13 | className?: string; 14 | home?: string; 15 | } 16 | 17 | export default function BreadcrumbSimple({ menu = [], className = "", home = "" }: Props) { 18 | const params = useParams(); 19 | return ( 20 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/routes/components/dropdowns.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Dropdown | Components | RemixBlocks 4 | description: RemixBlocks Dropdown Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Dropdown 22 | 23 | import PreviewDropdownsSimple from "~/components/ui/dropdowns/PreviewDropdownsSimple"; 24 | 25 | ### Dropdown - Simple 26 | 27 | 28 | 29 | ```tsx 30 | alert("Dropdown click")} 32 | button={
Dropdown
} 33 | options={ 34 |
35 | 43 | 44 |
Link
45 | 46 |
47 | } 48 | >
49 | ``` 50 | 51 | import PreviewDropdownsWithClick from "~/components/ui/dropdowns/PreviewDropdownsWithClick"; 52 | 53 | ### Dropdown - with Click 54 | 55 | 56 | 57 | ```tsx 58 | alert("Dropdown click")} 60 | button={
Dropdown with Click
} 61 | options={ 62 |
63 | 71 | 72 |
Link
73 | 74 |
75 | } 76 | /> 77 | ``` 78 | -------------------------------------------------------------------------------- /app/routes/blocks/lists/table/code.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Table | Code | RemixBlocks 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | ```tsx 9 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 10 | import { useLoaderData } from "@remix-run/react"; 11 | import TableSimple from "~/components/ui/tables/TableSimple"; 12 | 13 | interface EmployeeDto { 14 | id: number; 15 | firstName: string; 16 | lastName: string; 17 | email: string; 18 | phone: string; 19 | } 20 | 21 | type LoaderData = { 22 | title: string; 23 | items: EmployeeDto[]; 24 | }; 25 | export let loader: LoaderFunction = async () => { 26 | const items: EmployeeDto[] = [ 27 | { 28 | id: 1, 29 | firstName: "John", 30 | lastName: "Doe", 31 | email: "john.doe@company.com", 32 | phone: "555-555-5555", 33 | }, 34 | { 35 | id: 2, 36 | firstName: "Jane", 37 | lastName: "Deo", 38 | email: "jane.deo@company.com", 39 | phone: "777-777-777", 40 | }, 41 | ]; 42 | 43 | const data: LoaderData = { 44 | title: "Table | RemixBlocks", 45 | items, 46 | }; 47 | return json(data); 48 | }; 49 | 50 | export const meta: MetaFunction = ({ data }) => ({ 51 | title: data?.title, 52 | }); 53 | 54 | export default function Example() { 55 | const data = useLoaderData(); 56 | return ( 57 | alert(`${i.firstName} ${i.lastName}`), 63 | }, 64 | ]} 65 | headers={[ 66 | { 67 | name: "firstName", 68 | title: "First Name", 69 | value: (i) => i.firstName, 70 | }, 71 | { 72 | name: "lastName", 73 | title: "Last Name", 74 | value: (i) => i.lastName, 75 | }, 76 | { 77 | name: "email", 78 | title: "Email", 79 | value: (i) => i.email, 80 | className: "w-full", 81 | }, 82 | { 83 | name: "phone", 84 | title: "Phone", 85 | value: (i) => i.phone, 86 | breakpoint: "xl", 87 | }, 88 | ]} 89 | /> 90 | ); 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /app/components/ui/buttons/ButtonFlyout.tsx: -------------------------------------------------------------------------------- 1 | /* This example requires Tailwind CSS v2.0+ */ 2 | import { Fragment, ReactNode } from "react"; 3 | import { Popover, Transition } from "@headlessui/react"; 4 | import clsx from "clsx"; 5 | import ChevronDownIcon from "../icons/ChevronDownIcon"; 6 | 7 | interface Props { 8 | title: string; 9 | className?: string; 10 | disabled?: boolean; 11 | children: ReactNode; 12 | } 13 | export default function ButtonFlyout({ title, className, disabled, children }: Props) { 14 | return ( 15 | 16 | {({ open }) => ( 17 | <> 18 | 28 | {title} 29 | 31 | 32 | 41 | 42 |
43 |
{children}
44 |
45 |
46 |
47 | 48 | )} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/components/ui/buttons/ButtonShare.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, ReactNode } from "react"; 2 | import { Popover, Transition } from "@headlessui/react"; 3 | import clsx from "clsx"; 4 | import { useTranslation } from "react-i18next"; 5 | import ShareIcon from "../icons/ShareIcon"; 6 | 7 | interface Props { 8 | className?: string; 9 | disabled?: boolean; 10 | children: ReactNode; 11 | } 12 | export default function ButtonFlyout({ className, disabled, children }: Props) { 13 | const { t } = useTranslation(); 14 | return ( 15 | 16 | {({ open }) => ( 17 | <> 18 | 28 | {t("shared.share")} 29 | 31 | 32 | 41 | 42 |
43 |
{children}
44 |
45 |
46 |
47 | 48 | )} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/routes/blocks/email/contact-form-with-formspree/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; 3 | import { useLoaderData } from "@remix-run/react"; 4 | import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary"; 5 | import InputText from "~/components/ui/input/InputText"; 6 | 7 | type LoaderData = { 8 | title: string; 9 | actionUrl: string; 10 | }; 11 | export let loader: LoaderFunction = async () => { 12 | const data: LoaderData = { 13 | title: "Contact form with Formspree | RemixBlocks", 14 | actionUrl: process.env.INTEGRATIONS_CONTACT_FORMSPREE?.toString() ?? "", 15 | }; 16 | return json(data); 17 | }; 18 | 19 | export const meta: MetaFunction = ({ data }) => ({ 20 | title: data?.title, 21 | }); 22 | 23 | export default function Example() { 24 | const data = useLoaderData(); 25 | const [formUrl, setFormUrl] = useState(data.actionUrl); 26 | 27 | return ( 28 |
29 |
30 | {/* TODO: DELETE SET UP LINES - START */} 31 | 41 | Get form 42 | 43 | } 44 | /> 45 | 46 | {/* TODO: DELETE SET UP LINES - END */} 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 | Send 55 |
56 | 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/components/ui/modals/OpenModal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, Fragment } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import clsx from "clsx"; 4 | 5 | interface Props { 6 | children: ReactNode; 7 | onClose: () => void; 8 | className?: string; 9 | } 10 | export default function OpenModal({ children, onClose, className = "sm:max-w-3xl" }: Props) { 11 | return ( 12 | 13 | 14 |
15 | 24 | 25 | 26 | 27 | {/* This element is to trick the browser into centering the modal contents. */} 28 | 31 | 40 |
46 | {children} 47 |
48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/components/ui/input/InputCheckboxInline.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import { forwardRef, ReactNode, Ref, RefObject, useImperativeHandle, useRef } from "react"; 3 | import HintTooltip from "~/components/ui/tooltips/HintTooltip"; 4 | 5 | export interface RefInputCheckbox { 6 | input: RefObject; 7 | } 8 | 9 | interface Props { 10 | name: string; 11 | title: string; 12 | value?: boolean; 13 | setValue?: React.Dispatch>; 14 | className?: string; 15 | help?: string; 16 | required?: boolean; 17 | disabled?: boolean; 18 | description?: ReactNode; 19 | readOnly?: boolean; 20 | } 21 | const InputCheckboxInline = ( 22 | { name, title, value, setValue, description, className, help, required, disabled = false, readOnly }: Props, 23 | ref: Ref 24 | ) => { 25 | useImperativeHandle(ref, () => ({ input })); 26 | const input = useRef(null); 27 | return ( 28 |
29 |
30 |
31 | { 37 | setValue?.(e.target.checked); 38 | }} 39 | disabled={disabled} 40 | readOnly={readOnly} 41 | className={clsx( 42 | (disabled || readOnly) && "bg-gray-100 cursor-not-allowed", 43 | "cursor-pointer focus:ring-accent-500 h-4 w-4 text-accent-600 border-gray-300 rounded" 44 | )} 45 | /> 46 |
47 |
48 | 59 |
60 |
61 |
62 | ); 63 | }; 64 | export default forwardRef(InputCheckboxInline); 65 | -------------------------------------------------------------------------------- /app/routes/components/tables.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Tables | Components | RemixBlocks 4 | description: RemixBlocks Table Components 5 | headers: 6 | Cache-Control: no-cache 7 | --- 8 | 9 | import BlocksBreadcrumb from "~/components/blocks/BlocksBreadcrumb"; 10 | 11 | 20 | 21 | # Tables 22 | 23 | import PreviewTableSimple from "~/components/ui/tables/PreviewTableSimple"; 24 | 25 | ### Tabs - Simple 26 | 27 | 28 | 29 | ```tsx 30 | import DateUtils from "~/utils/shared/DateUtils"; 31 | import NumberUtils from "~/utils/shared/NumberUtils"; 32 | import TableSimple from "./TableSimple"; 33 | 34 | const items = [ 35 | { 36 | firstName: "John", 37 | lastName: "Doe", 38 | email: "john.doe@company.com", 39 | salary: 100, 40 | birthday: new Date(1990, 1, 1), 41 | }, 42 | { 43 | firstName: "Luna", 44 | lastName: "Davis", 45 | email: "luna.davis@company.com", 46 | salary: 100.5, 47 | birthday: new Date(1990, 12, 31), 48 | }, 49 | ]; 50 | export default function PreviewTableSimple() { 51 | return ( 52 | `${item.firstName} ${item.lastName}`, 59 | }, 60 | { 61 | title: "Email", 62 | name: "email", 63 | value: (item) => item.email, 64 | }, 65 | { 66 | title: "Salary", 67 | name: "salary", 68 | className: "text-blue-500", 69 | value: (item) => item.salary, 70 | formattedValue: (item) => NumberUtils.decimalFormat(item.salary), 71 | }, 72 | { 73 | title: "Birthday", 74 | name: "birthday", 75 | className: "text-gray-400", 76 | value: (item) => item.birthday, 77 | formattedValue: (item) => DateUtils.dateYMD(item.birthday), 78 | }, 79 | ]} 80 | actions={[ 81 | { 82 | title: "View", 83 | onClick: (idx, item) => alert("Clicked: " + item.firstName), 84 | }, 85 | ]} 86 | > 87 | ); 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /app/routes/blocks/email/contact-form-with-formspree/code.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | title: Contact form with Formspree | Code | RemixBlocks 4 | headers: 5 | Cache-Control: no-cache 6 | --- 7 | 8 | ```tsx 9 | import { useState } from "react"; 10 | import { json, LoaderFunction, MetaFunction, useLoaderData } from "remix"; 11 | import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary"; 12 | import InputText from "~/components/ui/input/InputText"; 13 | 14 | type LoaderData = { 15 | title: string; 16 | actionUrl: string; 17 | }; 18 | export let loader: LoaderFunction = async () => { 19 | const data: LoaderData = { 20 | title: "Contact form with Formspree | RemixBlocks", 21 | actionUrl: process.env.INTEGRATIONS_CONTACT_FORMSPREE?.toString() ?? "", 22 | }; 23 | return json(data); 24 | }; 25 | 26 | export const meta: MetaFunction = ({ data }) => ({ 27 | title: data?.title, 28 | }); 29 | 30 | export default function Example() { 31 | const data = useLoaderData(); 32 | const [formUrl, setFormUrl] = useState(data.actionUrl); 33 | 34 | return ( 35 |
36 |
37 | {/* TODO: DELETE SET UP LINES - START */} 38 | 48 | Get form 49 | 50 | } 51 | /> 52 | 53 | {/* TODO: DELETE SET UP LINES - END */} 54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 | Send 62 |
63 | 64 |
65 | ); 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /app/components/blocks/BlockDemoCodeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from "react"; 2 | import { useLocation, useNavigate } from "@remix-run/react"; 3 | import InputRadioGroup from "../ui/input/InputRadioGroup"; 4 | 5 | export default function BlockDemoCodeToggle() { 6 | const [value, setValue] = useState(""); 7 | const navigate = useNavigate(); 8 | const location = useLocation(); 9 | 10 | useEffect(() => { 11 | const current = tabs.find((element) => element.value && (location.pathname + location.search).includes(element.value)); 12 | if (current) { 13 | setValue(current.value); 14 | } 15 | // eslint-disable-next-line react-hooks/exhaustive-deps 16 | }, [location.pathname]); 17 | 18 | useEffect(() => { 19 | navigate(value); 20 | // eslint-disable-next-line react-hooks/exhaustive-deps 21 | }, [value]); 22 | 23 | return setValue(e?.toString() ?? "")} options={tabs} />; 24 | } 25 | 26 | const tabs: { name: string | ReactNode; value: string }[] = [ 27 | { 28 | name: ( 29 |
30 | 31 | 36 | 37 |
Demo
38 |
39 | ), 40 | value: "", 41 | }, 42 | { 43 | name: ( 44 |
45 | 46 | 51 | 52 |
Code
53 |
54 | ), 55 | value: "code", 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /app/components/ui/dropdowns/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, MouseEventHandler, ReactNode } from "react"; 2 | import { Menu, Transition } from "@headlessui/react"; 3 | import clsx from "clsx"; 4 | 5 | interface Props { 6 | right?: boolean; 7 | button?: ReactNode; 8 | options?: ReactNode; 9 | children?: ReactNode; 10 | className?: string; 11 | btnClassName?: string; 12 | onClick?: MouseEventHandler; 13 | } 14 | 15 | export default function Dropdown({ button, options, right, onClick, className, btnClassName }: Props) { 16 | return ( 17 | 18 |
19 | 26 | {button} 27 | {!btnClassName && ( 28 | 29 | 34 | 35 | )} 36 | 37 |
38 | 39 | 48 | 54 |
{options}
55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/components/ui/modals/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, Fragment } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import clsx from "clsx"; 4 | 5 | interface Props { 6 | className?: string; 7 | children: ReactNode; 8 | open: boolean; 9 | setOpen: React.Dispatch>; 10 | } 11 | export default function Modal({ className, children, open, setOpen }: Props) { 12 | function onClose() { 13 | setOpen(false); 14 | } 15 | return ( 16 | 17 | 18 |
19 | 28 | 29 | 30 | 31 | {/* This element is to trick the browser into centering the modal contents. */} 32 | 35 | 44 |
50 | {children} 51 |
52 |
53 |
54 |
55 |
56 | ); 57 | } 58 | --------------------------------------------------------------------------------