├── 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 |
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 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/app/components/ui/input/previews/PreviewInputText.tsx:
--------------------------------------------------------------------------------
1 | import InputText from "../InputText";
2 |
3 | export default function PreviewInputText() {
4 | return (
5 |
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 |
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 |
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 |
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 |
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 |
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 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/ui/icons/LockClosedIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function LockClosedIcon({ className }: { className?: string }) {
2 | return (
3 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/app/components/ui/icons/ShareIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function ShareIcon({ className }: { className?: string }) {
2 | return (
3 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/app/components/ui/loaders/PreviewLoaders.tsx:
--------------------------------------------------------------------------------
1 | import Loading from "./Loading";
2 |
3 | export default function PreviewLoaders() {
4 | return (
5 |
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 |
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 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/ui/icons/RightIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function RightIcon({ className }: { className?: string }) {
2 | return (
3 |
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 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/ui/icons/PencilAltIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function PencilAltIcon({ className }: { className?: string }) {
2 | return (
3 |
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 |
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 |
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 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/ui/icons/XIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function XIcon({ className }: { className?: string }) {
2 | return (
3 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/ui/icons/DownloadIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function DownloadIcon({ className }: { className?: string }) {
2 | return (
3 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/ui/icons/ViewListIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function ViewList({ className }: { className?: string }) {
2 | return (
3 |
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 |
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 |
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 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/ui/icons/OptionsIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function OptionsIcon() {
2 | return (
3 |
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 |
12 |
13 | {children}
14 |
15 |
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 |
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 |
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 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/ui/icons/EyeIcon.tsx:
--------------------------------------------------------------------------------
1 | export default function EyeIcon({ className }: { className?: string }) {
2 | return (
3 |
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 |
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 |
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 |
16 |
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 |
16 |
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 |
12 |
13 |
14 |
15 |
Tab - as Buttons
16 |
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 |
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 |
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 |
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 |
7 |
8 |
Dropdowns - Simple
9 |
12 |
13 |
14 |
20 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |

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

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 |
7 |
8 |
9 |

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

16 |
17 |
18 |
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 |
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 |

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 | {/*
*/}
16 |
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 |
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 |
9 |
Breadcrumb
10 |
11 |
20 |
21 |
22 |
BreadcrumbSimple
23 |
24 |
33 |
34 |
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 | 
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 |
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 |
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 |
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 |
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 |
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 |

16 |
17 |
18 |

19 |
20 |
21 |

22 |
23 |
24 |

25 |
26 |
27 |

28 |
29 |
30 |

31 |
32 |
33 |

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 |
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 |
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 |
49 | ) : isImage() ? (
50 |
51 |

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 |
30 |
31 |
32 |
41 |
42 |
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 |
30 |
31 |
32 |
41 |
42 |
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 |
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 |
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 |
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 |
37 |
Demo
38 |
39 | ),
40 | value: "",
41 | },
42 | {
43 | name: (
44 |
45 |
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 |
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 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------