├── .husky ├── .gitignore ├── commit-msg ├── pre-commit └── prepare-commit-msg ├── commitlint.config.js ├── .gitignore ├── .eslintignore ├── .prettierignore ├── .babelrc ├── public ├── fonts │ ├── Inter-Bold.ttf │ ├── Inter-Regular.ttf │ └── Inter-SemiBold.ttf └── logo.svg ├── src ├── contexts │ └── GlobalApp │ │ ├── index.ts │ │ ├── Context.ts │ │ ├── Provider.tsx │ │ ├── initialState.ts │ │ ├── types.ts │ │ └── reducer.ts ├── components │ ├── global │ │ ├── DataDisplay │ │ │ ├── Text │ │ │ │ └── index.ts │ │ │ ├── Title │ │ │ │ └── index.tsx │ │ │ └── TypographyBase │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ ├── Icon │ │ │ ├── ThunderFilled.tsx │ │ │ ├── CloseFilled.tsx │ │ │ ├── FacebookOutlined.tsx │ │ │ ├── PencilOutlined.tsx │ │ │ ├── create-icon.tsx │ │ │ ├── styled.ts │ │ │ ├── InstagramOutlined.tsx │ │ │ ├── TwitterOutlined.tsx │ │ │ └── TrashOutlined.tsx │ │ ├── Form │ │ │ ├── IconButton │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── Button │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── ButtonBase │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ ├── Switch │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ └── Input │ │ │ │ └── index.ts │ │ ├── Layout │ │ │ ├── DasboardLayout │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ │ └── Spacing │ │ │ │ └── index.ts │ │ ├── Navigation │ │ │ ├── Sidebar │ │ │ │ └── index.tsx │ │ │ └── Tabs │ │ │ │ ├── styled.ts │ │ │ │ └── index.tsx │ │ └── Media │ │ │ └── Avatar.tsx │ └── local │ │ └── home │ │ ├── editing │ │ ├── SettingsTab │ │ │ ├── index.tsx │ │ │ └── SocialLinks.tsx │ │ ├── AppearanceTab │ │ │ ├── index.tsx │ │ │ ├── Profile │ │ │ │ ├── styled.ts │ │ │ │ └── index.tsx │ │ │ └── Themes │ │ │ │ ├── index.tsx │ │ │ │ └── styled.ts │ │ ├── ItemCard │ │ │ ├── styled.ts │ │ │ └── index.tsx │ │ └── LinksTab │ │ │ └── index.tsx │ │ └── previewLive │ │ ├── styled.ts │ │ └── index.tsx ├── pages │ ├── index.tsx │ ├── _app.tsx │ └── _document.tsx ├── styles │ └── global.css ├── modules │ └── home │ │ ├── Preview │ │ └── index.tsx │ │ └── Editing │ │ ├── styled.ts │ │ └── index.tsx ├── mocks │ └── themes.ts └── theme │ ├── index.ts │ └── global-styles.ts ├── .prettierrc ├── README.md ├── tsconfig.json ├── LICENSE ├── styled.d.ts ├── .eslintrc └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env.* 3 | .next/ 4 | next-env.d.ts 5 | package-lock.json 6 | .vercel 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env.* 3 | .next/ 4 | next-env.d.ts 5 | package-lock.json 6 | package.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env.* 3 | .next/ 4 | next-env.d.ts 5 | package-lock.json 6 | package.json -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [["styled-components", { "ssr": true }]] 4 | } 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | npm run format 6 | git add . 7 | -------------------------------------------------------------------------------- /public/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexGarrixen/linktree_react_clone/HEAD/public/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexGarrixen/linktree_react_clone/HEAD/public/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexGarrixen/linktree_react_clone/HEAD/public/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && node_modules/.bin/cz --hook || true 5 | -------------------------------------------------------------------------------- /src/contexts/GlobalApp/index.ts: -------------------------------------------------------------------------------- 1 | import AppContext from './Context'; 2 | import Provider from './Provider'; 3 | 4 | export { AppContext, Provider }; 5 | -------------------------------------------------------------------------------- /src/components/global/DataDisplay/Text/index.ts: -------------------------------------------------------------------------------- 1 | import TypographyBase from '@components/DataDisplay/TypographyBase'; 2 | 3 | const Title = TypographyBase; 4 | 5 | export default Title; 6 | -------------------------------------------------------------------------------- /src/contexts/GlobalApp/Context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { ContextValue } from './types'; 3 | 4 | const AppContext = createContext>({}); 5 | 6 | export default AppContext; 7 | -------------------------------------------------------------------------------- /src/components/local/home/editing/SettingsTab/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SocialLinks from './SocialLinks'; 3 | 4 | const SettingsTab = () => ( 5 |
6 | 7 |
8 | ); 9 | 10 | export default SettingsTab; 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "printWidth": 80, 7 | "arrowParens": "always", 8 | "endOfLine": "lf", 9 | "useTabs": false, 10 | "bracketSpacing": true 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linktree React Clone 2 | 3 | Clone of linktree app 4 | 5 | ![Thumbnail](https://res.cloudinary.com/dxarbtyux/image/upload/v1625159842/linktree_react_qj2iaz.jpg) 6 | 7 | # How to run locally 8 | 9 | ```bash 10 | $ npm install 11 | $ npm run dev 12 | ``` 13 | -------------------------------------------------------------------------------- /src/components/global/Icon/ThunderFilled.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createIcon from './create-icon'; 3 | 4 | const ThunderFilled = createIcon( 5 | 6 | ); 7 | 8 | export default ThunderFilled; 9 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DashboardLayout from '@components/Layout/DasboardLayout'; 3 | import Editing from '@modules/home/Editing'; 4 | import Preview from '@modules/home/Preview'; 5 | 6 | const Dashboard = () => ( 7 | } 9 | PreviewComponent={} 10 | /> 11 | ); 12 | 13 | export default Dashboard; 14 | -------------------------------------------------------------------------------- /src/components/global/Icon/CloseFilled.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createIcon from './create-icon'; 3 | 4 | const CloseFilled = () => 5 | createIcon( 6 | 7 | ); 8 | 9 | export default CloseFilled; 10 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | src: url('/fonts/Inter-Bold.ttf'); 4 | font-weight: 700; 5 | font-display: swap; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Inter'; 10 | src: url('/fonts/Inter-SemiBold.ttf'); 11 | font-weight: 600; 12 | font-display: swap; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Inter'; 17 | src: url('/fonts/Inter-Regular.ttf'); 18 | font-weight: 400; 19 | font-display: swap; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/local/home/editing/AppearanceTab/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Profile from './Profile'; 4 | import Themes from './Themes'; 5 | 6 | const AppearanceTabRoot = styled.div` 7 | display: grid; 8 | gap: 48px; 9 | `; 10 | 11 | const AppearanceTab = () => ( 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default AppearanceTab; 19 | -------------------------------------------------------------------------------- /src/contexts/GlobalApp/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer, FC } from 'react'; 2 | import ContextApp from './Context'; 3 | import reducer from './reducer'; 4 | import initialState from './initialState'; 5 | 6 | const Provider: FC = ({ children }) => { 7 | const [state, dispatch] = useReducer(reducer, initialState); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default Provider; 17 | -------------------------------------------------------------------------------- /src/modules/home/Preview/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PreviewLive from '@localComponents/home/previewLive'; 4 | 5 | const PreviewRoot = styled.div` 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | padding: 20px; 10 | align-items: center; 11 | justify-content: center; 12 | `; 13 | 14 | const Preview = () => ( 15 | 16 | 17 | 18 | ); 19 | 20 | export default Preview; 21 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppProps } from 'next/app'; 3 | import { ThemeProvider } from 'styled-components'; 4 | import theme, { GlobalStyle } from '@theme/index'; 5 | import { Provider } from '@contexts/GlobalApp'; 6 | import '../styles/global.css'; 7 | 8 | function App({ Component, pageProps }: AppProps) { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/components/global/Form/IconButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ButtonBaseProps } from '@components/Form/ButtonBase'; 3 | import { IconButtonRoot } from './styled'; 4 | 5 | export type IconButtonProps = { 6 | as?: React.ElementType | string; 7 | size?: 'sm' | 'md' | 'lg'; 8 | } & ButtonBaseProps; 9 | 10 | const IconButton = ({ 11 | size = 'md', 12 | children, 13 | colorScheme = 'base', 14 | ...rest 15 | }: IconButtonProps) => ( 16 | 17 | {children} 18 | 19 | ); 20 | 21 | export default IconButton; 22 | -------------------------------------------------------------------------------- /src/components/global/Icon/FacebookOutlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createIcon from './create-icon'; 3 | 4 | const FacebookOutlined = createIcon( 5 | 6 | ); 7 | 8 | export default FacebookOutlined; 9 | -------------------------------------------------------------------------------- /src/contexts/GlobalApp/initialState.ts: -------------------------------------------------------------------------------- 1 | import { State } from './types'; 2 | import mockThemes from '@mocks/themes'; 3 | 4 | const initialState: State = { 5 | appearance: { 6 | profile: { 7 | avatar: 8 | 'https://pbs.twimg.com/profile_images/453956388851445761/8BKnRUXg_400x400.png', 9 | username: 'usuario', 10 | description: 'descripcion', 11 | }, 12 | themes: { 13 | selected: null, 14 | list: mockThemes, 15 | }, 16 | }, 17 | config: { 18 | social: { 19 | facebook: '', 20 | instagram: '', 21 | twitter: '', 22 | }, 23 | }, 24 | items: [], 25 | }; 26 | 27 | export default initialState; 28 | -------------------------------------------------------------------------------- /src/components/global/Form/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ButtonBaseProps } from '@components/Form/ButtonBase'; 3 | import { ButtonRoot, Label } from './styled'; 4 | 5 | export type ButtonProps = { 6 | size?: 'sm' | 'md'; 7 | fullWidth?: boolean; 8 | } & ButtonBaseProps; 9 | 10 | const Button = ({ 11 | colorScheme = 'base', 12 | size = 'md', 13 | children, 14 | fullWidth, 15 | ...rest 16 | }: ButtonProps) => ( 17 | 23 | 24 | 25 | ); 26 | 27 | export default Button; 28 | -------------------------------------------------------------------------------- /src/components/global/Layout/DasboardLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DashboardRoot, ContentBox } from './styled'; 3 | import Sidebar from '@components/Navigation/Sidebar'; 4 | 5 | type DashboardLayoutProps = { 6 | EditingComponent?: React.ReactNode; 7 | PreviewComponent?: React.ReactNode; 8 | }; 9 | 10 | const DashboardLayout = ({ 11 | EditingComponent, 12 | PreviewComponent, 13 | }: DashboardLayoutProps) => ( 14 | 15 | 16 | 17 |
{EditingComponent}
18 |
{PreviewComponent}
19 |
20 |
21 | ); 22 | 23 | export default DashboardLayout; 24 | -------------------------------------------------------------------------------- /src/modules/home/Editing/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const EditingRoot = styled.div` 4 | height: 100%; 5 | border-right: 1px solid ${({ theme }) => theme.colors.gray[200]}; 6 | display: flex; 7 | flex-direction: column; 8 | `; 9 | 10 | export const TabsBox = styled.div` 11 | padding: 0 20px; 12 | background-color: ${({ theme }) => theme.colors.white}; 13 | border-bottom: 1px solid ${({ theme }) => theme.colors.gray[200]}; 14 | `; 15 | 16 | export const ContentBox = styled.div` 17 | flex: 1; 18 | overflow-y: auto; 19 | padding: 24px; 20 | 21 | & > div { 22 | max-width: 700px; 23 | width: 100%; 24 | margin: 0 auto; 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/components/global/Form/IconButton/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import ButtonBase, { ButtonBaseProps } from '@components/Form/ButtonBase'; 3 | 4 | export const IconButtonRoot = styled< 5 | (props: ButtonBaseProps<{ size?: 'sm' | 'md' | 'lg' }>) => JSX.Element 6 | >(ButtonBase)` 7 | display: inline-flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | ${({ size }) => 12 | size === 'sm' && 13 | ` 14 | width: 30px; 15 | height: 30px; 16 | `} 17 | 18 | ${({ size }) => 19 | size === 'md' && 20 | ` 21 | width: 40px; 22 | height: 40px; 23 | `} 24 | 25 | ${({ size }) => 26 | size === 'lg' && 27 | ` 28 | width: 48px; 29 | height: 48px; 30 | `} 31 | `; 32 | -------------------------------------------------------------------------------- /src/components/global/Layout/DasboardLayout/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const DashboardRoot = styled.div` 4 | height: 100vh; 5 | overflow: hidden; 6 | display: grid; 7 | grid-template-columns: 1fr; 8 | background-color: ${({ theme }) => theme.colors.gray[100]}; 9 | 10 | @media screen and (min-width: ${({ theme }) => theme.screens.md}) { 11 | grid-template-columns: 64px 1fr; 12 | } 13 | `; 14 | 15 | export const ContentBox = styled.div` 16 | overflow-y: auto; 17 | overflow-x: hidden; 18 | 19 | @media screen and (min-width: ${({ theme }) => theme.screens.md}) { 20 | height: 100%; 21 | display: grid; 22 | grid-template-columns: 1fr 0.6fr; 23 | 24 | & > section { 25 | overflow-y: hidden; 26 | } 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /src/components/global/Navigation/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const SidebarRoot = styled.aside` 5 | height: 100%; 6 | overflow-y: auto; 7 | text-align: center; 8 | background-color: ${({ theme }) => theme.colors.white}; 9 | padding: 20px 6px; 10 | border-bottom: 1px solid ${({ theme }) => theme.colors.gray[200]}; 11 | 12 | img { 13 | width: 24px; 14 | } 15 | 16 | @media screen and (min-width: ${({ theme }) => theme.screens.md}) { 17 | border-right: 1px solid ${({ theme }) => theme.colors.gray[200]}; 18 | border-bottom: 0; 19 | } 20 | `; 21 | 22 | const Sidebar = () => ( 23 | 24 | logo 25 | 26 | ); 27 | 28 | export default Sidebar; 29 | -------------------------------------------------------------------------------- /src/mocks/themes.ts: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid'; 2 | 3 | const mockThemes = [ 4 | { 5 | id: shortid.generate(), 6 | name: 'Summer', 7 | src: 'https://res.cloudinary.com/dxarbtyux/image/upload/v1625095575/Summer_r4bq2g.svg', 8 | }, 9 | { 10 | id: shortid.generate(), 11 | name: 'Pink', 12 | src: 'https://res.cloudinary.com/dxarbtyux/image/upload/v1625095575/Pink_xnhz7r.svg', 13 | }, 14 | { 15 | id: shortid.generate(), 16 | name: 'Ocean', 17 | src: 'https://res.cloudinary.com/dxarbtyux/image/upload/v1625095575/Ocean_di3ett.svg', 18 | }, 19 | { 20 | id: shortid.generate(), 21 | name: 'Purple', 22 | src: 'https://res.cloudinary.com/dxarbtyux/image/upload/v1625095575/Purple_ug8jf7.svg', 23 | }, 24 | ]; 25 | 26 | export default mockThemes; 27 | -------------------------------------------------------------------------------- /src/components/global/Icon/PencilOutlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createIcon from './create-icon'; 3 | 4 | const PencilOutlined = createIcon( 5 | 6 | ); 7 | 8 | export default PencilOutlined; 9 | -------------------------------------------------------------------------------- /src/components/global/Media/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | type AvatarProps = { 4 | size?: 'sm' | 'md' | 'lg' | number; 5 | }; 6 | 7 | const sizes = { 8 | lg: 48, 9 | md: 32, 10 | sm: 24, 11 | }; 12 | 13 | const setSize = (sizeArg?: AvatarProps['size']) => { 14 | if (typeof sizeArg === 'string') { 15 | const size = sizes[sizeArg]; 16 | return ` 17 | width: ${size}px; 18 | height: ${size}px; 19 | `; 20 | } 21 | 22 | return ` 23 | width: ${sizeArg}px; 24 | height: ${sizeArg}px; 25 | `; 26 | }; 27 | 28 | const Avatar = styled.img` 29 | background-color: ${({ theme }) => theme.colors.gray[200]}; 30 | border-radius: 50%; 31 | object-fit: cover; 32 | object-position: center; 33 | ${({ size = 'md' }) => size && setSize(size)} 34 | `; 35 | 36 | export default Avatar; 37 | -------------------------------------------------------------------------------- /src/components/global/Form/Button/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import ButtonBase, { ButtonBaseProps } from '@components/Form/ButtonBase'; 3 | 4 | export const ButtonRoot = styled< 5 | ( 6 | props: ButtonBaseProps<{ size?: 'sm' | 'md'; fullWidth?: boolean }> 7 | ) => JSX.Element 8 | >(ButtonBase)` 9 | display: inline-flex; 10 | align-items: center; 11 | justify-content: center; 12 | 13 | ${({ fullWidth }) => fullWidth && 'width: 100%;'} 14 | 15 | ${({ size }) => 16 | size === 'md' && 17 | ` 18 | padding: 10px 16px; 19 | `} 20 | 21 | ${({ size, theme }) => 22 | size === 'sm' && 23 | ` 24 | padding: 6px 12px; 25 | font-size: ${theme.fontSizes.sm.size}; 26 | `} 27 | `; 28 | 29 | export const Label = styled.span` 30 | font-size: inherit; 31 | font-weight: inherit; 32 | color: inherit; 33 | `; 34 | -------------------------------------------------------------------------------- /src/components/global/DataDisplay/Title/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TypographyBase, { 3 | TypographyProps, 4 | } from '@components/DataDisplay/TypographyBase'; 5 | 6 | type TitleProps = { 7 | level?: '1' | '2' | '3' | '4' | '5' | '6'; 8 | } & TypographyProps; 9 | 10 | const levels: Record< 11 | string, 12 | keyof Pick 13 | > = { 14 | '1': 'h1', 15 | '2': 'h2', 16 | '3': 'h3', 17 | '4': 'h4', 18 | '5': 'h5', 19 | '6': 'h6', 20 | }; 21 | 22 | const Title = ({ 23 | as, 24 | level, 25 | colorScheme = 'secondary', 26 | ...rest 27 | }: TitleProps) => { 28 | const fallbackAs = levels['1']; 29 | 30 | return ( 31 | 36 | ); 37 | }; 38 | 39 | export default Title; 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@theme/*": ["src/theme/*"], 19 | "@components/*": ["src/components/global/*"], 20 | "@localComponents/*": ["src/components/local/*"], 21 | "@modules/*": ["src/modules/*"], 22 | "@mocks/*": ["src/mocks/*"], 23 | "@contexts/*": ["src/contexts/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/global/DataDisplay/TypographyBase/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DefaultTheme } from 'styled-components'; 3 | import { TypographyRoot } from './styled'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | export type TypographyProps

= { 7 | children?: React.ReactNode; 8 | as?: React.ElementType; 9 | colorScheme?: 'primary' | 'secondary' | 'danger'; 10 | className?: string; 11 | color?: string; 12 | size?: 13 | | keyof DefaultTheme['fontSizes'] 14 | | Partial< 15 | Record 16 | >; 17 | } & P; 18 | 19 | const TypographyBase = ({ 20 | as, 21 | colorScheme, 22 | color, 23 | size, 24 | children, 25 | }: TypographyProps) => ( 26 | 27 | {children} 28 | 29 | ); 30 | 31 | export default TypographyBase; 32 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { DocumentContext } from 'next/document'; 3 | import { ServerStyleSheet } from 'styled-components'; 4 | 5 | class MyDocument extends Document { 6 | static async getInitialProps(ctx: DocumentContext) { 7 | const sheet = new ServerStyleSheet(); 8 | const originalRenderPage = ctx.renderPage; 9 | 10 | try { 11 | ctx.renderPage = () => 12 | originalRenderPage({ 13 | enhanceApp: (App) => (props) => 14 | sheet.collectStyles(), 15 | }); 16 | 17 | const initialProps = await Document.getInitialProps(ctx); 18 | 19 | return { 20 | ...initialProps, 21 | styles: ( 22 | <> 23 | {initialProps.styles} 24 | {sheet.getStyleElement()} 25 | 26 | ), 27 | }; 28 | } finally { 29 | sheet.seal(); 30 | } 31 | } 32 | } 33 | 34 | export default MyDocument; 35 | -------------------------------------------------------------------------------- /src/components/global/Form/ButtonBase/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ButtonBaseRoot } from './styled'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/ban-types 5 | export type ButtonBaseProps

= { 6 | as?: React.ElementType | string; 7 | colorScheme?: 'primary' | 'secondary' | 'base' | 'white'; 8 | color?: string; 9 | bgColor?: string; 10 | variant?: 'filled' | 'outlined'; 11 | onClick?: (e: React.MouseEvent) => void; 12 | children?: React.ReactNode; 13 | className?: string; 14 | disabled?: boolean; 15 | } & P; 16 | 17 | const ButtonBase = ({ 18 | as, 19 | colorScheme, 20 | color, 21 | bgColor, 22 | variant = 'filled', 23 | children, 24 | ...rest 25 | }: ButtonBaseProps) => ( 26 | 34 | {children} 35 | 36 | ); 37 | 38 | export default ButtonBase; 39 | -------------------------------------------------------------------------------- /src/components/local/home/editing/AppearanceTab/Profile/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ContentBox = styled.div` 4 | background-color: ${({ theme }) => theme.colors.white}; 5 | border-radius: ${({ theme }) => theme.borderRadius.base}; 6 | padding: 20px; 7 | box-shadow: 0px 4px 4px ${({ theme }) => theme.colors.gray[200]}; 8 | `; 9 | 10 | export const HeadingBox = styled.div` 11 | & > div { 12 | margin-top: 12px; 13 | display: grid; 14 | gap: 16px; 15 | } 16 | 17 | label { 18 | cursor: pointer; 19 | 20 | button { 21 | pointer-events: none; 22 | } 23 | } 24 | 25 | @media screen and (min-width: ${({ theme }) => theme.screens.md}) { 26 | display: flex; 27 | align-items: center; 28 | gap: 20px; 29 | 30 | & > div { 31 | flex: 1; 32 | grid-template-columns: repeat(2, 1fr); 33 | margin-top: 0; 34 | } 35 | } 36 | `; 37 | 38 | export const InputsBox = styled.div` 39 | display: grid; 40 | gap: 16px; 41 | `; 42 | -------------------------------------------------------------------------------- /src/components/global/Icon/create-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Svg } from './styled'; 3 | 4 | export type IconProps = { 5 | size?: 'xs' | 'sm' | '1x' | '2x' | '3x' | '4x' | number; 6 | colorScheme?: 'primary' | 'white' | 'danger'; 7 | color?: string; 8 | height?: number | string; 9 | width?: number | string; 10 | }; 11 | 12 | const createIcon = (path?: React.ReactNode) => { 13 | const Icon = ({ 14 | size = '1x', 15 | color, 16 | colorScheme, 17 | height = '1em', 18 | width = '1em', 19 | viewBox = '0 0 1024 1024', 20 | fill = 'currentColor', 21 | ...rest 22 | }: IconProps & 23 | Omit, keyof IconProps>) => ( 24 | 34 | {path} 35 | 36 | ); 37 | 38 | return Icon; 39 | }; 40 | 41 | export default createIcon; 42 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/global/Form/Switch/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from 'react'; 2 | import { SwitchRoot, Indicator } from './styled'; 3 | 4 | export type SwitchProps = { 5 | onSwitch?: (isEnabled: boolean) => void; 6 | className?: string; 7 | colorScheme?: 'primary' | 'success'; 8 | isEnabled?: boolean; 9 | }; 10 | 11 | const Switch = ({ 12 | onSwitch, 13 | isEnabled = false, 14 | colorScheme = 'primary', 15 | ...rest 16 | }: SwitchProps) => { 17 | const [enabled, setEnabled] = useState(isEnabled); 18 | const handleSwitch = useCallback(() => setEnabled(!enabled), [enabled]); 19 | const mounted = useRef(false); 20 | 21 | useEffect(() => { 22 | if (mounted.current) onSwitch && onSwitch(enabled); 23 | mounted.current = true; 24 | }, [enabled, mounted.current]); 25 | 26 | return ( 27 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default Switch; 39 | -------------------------------------------------------------------------------- /src/components/local/home/previewLive/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const PreviewRoot = styled.div<{ srcBg?: string }>` 4 | border: 14px solid ${({ theme }) => theme.colors.gray[500]}; 5 | background-size: cover; 6 | background-position: center; 7 | width: 100%; 8 | max-width: 300px; 9 | height: 600px; 10 | border-radius: 42px; 11 | text-align: center; 12 | overflow: hidden; 13 | 14 | ${({ srcBg }) => 15 | srcBg && 16 | ` 17 | background-image: url(${srcBg}); 18 | `} 19 | 20 | & > div { 21 | padding: 28px 16px; 22 | overflow-y: auto; 23 | max-height: 100%; 24 | } 25 | `; 26 | 27 | export const ProfileBox = styled.div` 28 | display: grid; 29 | gap: 4px; 30 | justify-items: center; 31 | `; 32 | 33 | export const ItemsBox = styled.ul` 34 | display: grid; 35 | gap: 12px; 36 | `; 37 | 38 | export const SocialNetworksBox = styled.ul` 39 | display: flex; 40 | justify-content: center; 41 | gap: 6px; 42 | color: #fff; 43 | align-items: center; 44 | 45 | li { 46 | display: inherit; 47 | align-items: inherit; 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AlexGarrixen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /styled.d.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | 3 | declare module 'styled-components' { 4 | export interface DefaultTheme { 5 | colors: { 6 | primary: string; 7 | success: string; 8 | danger: string; 9 | white: string; 10 | gray: { 11 | 100: string; 12 | 200: string; 13 | 300: string; 14 | 400: string; 15 | 500: string; 16 | }; 17 | }; 18 | screens: { 19 | xs: string; 20 | sm: string; 21 | md: string; 22 | lg: string; 23 | xl: string; 24 | }; 25 | fontSizes: { 26 | xs: { size: string; lineHeight: string }; 27 | sm: { size: string; lineHeight: string }; 28 | base: { size: string; lineHeight: string }; 29 | lg: { size: string; lineHeight: string }; 30 | xl: { size: string; lineHeight: string }; 31 | '2xl': { size: string; lineHeight: string }; 32 | '3xl': { size: string; lineHeight: string }; 33 | '4xl': { size: string; lineHeight: string }; 34 | '5xl': { size: string; lineHeight: string }; 35 | }; 36 | fontFamily: { 37 | sans: string; 38 | }; 39 | borderRadius: { 40 | base: string; 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/global/Form/Input/index.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | type InputProps = { 4 | fullWidth?: boolean; 5 | size?: 'sm' | 'md'; 6 | error?: boolean; 7 | }; 8 | 9 | const Input = styled.input.attrs(() => ({ size: 'md' }))` 10 | background-color: ${({ theme }) => theme.colors.gray[100]}; 11 | color: ${({ theme }) => theme.colors.gray[400]}; 12 | border: none; 13 | border-radius: ${({ theme }) => theme.borderRadius.base}; 14 | 15 | ${({ fullWidth }) => fullWidth && 'width: 100%;'} 16 | 17 | ${({ error, theme }) => 18 | error && 19 | ` 20 | box-shadow: 0 0 0 2px ${theme.colors.danger}; 21 | `} 22 | 23 | ${({ disabled }) => 24 | disabled && 25 | ` 26 | opacity: 0.4; 27 | & > * { 28 | cursor: not-allowed; 29 | } 30 | `} 31 | 32 | ${({ size, theme }) => 33 | size === 'sm' && 34 | ` 35 | min-height: 32px; 36 | padding: 6px 12px; 37 | font-size: ${theme.fontSizes.sm.size}; 38 | `} 39 | 40 | ${({ size, theme }) => 41 | size === 'md' && 42 | ` 43 | font-size: ${theme.fontSizes.sm.size}; 44 | min-height: 42px; 45 | padding: 8px 16px; 46 | `} 47 | `; 48 | 49 | export default Input; 50 | -------------------------------------------------------------------------------- /src/components/global/Layout/Spacing/index.ts: -------------------------------------------------------------------------------- 1 | import styled, { DefaultTheme, css } from 'styled-components'; 2 | 3 | type Breakpoints = Partial>; 4 | 5 | type SpacingProps = { 6 | size?: number | Breakpoints; 7 | }; 8 | 9 | const setSpacing = (size) => css` 10 | padding-top: ${size}px; 11 | `; 12 | 13 | const setBreakpointsSpacing = ( 14 | breakpointsConfig: Breakpoints, 15 | theme: DefaultTheme 16 | ) => { 17 | const screens = Object.keys(breakpointsConfig); 18 | const rules: string[] = []; 19 | 20 | for (const screen of screens) { 21 | const breakpoint = theme.screens[screen]; 22 | 23 | if (breakpoint) { 24 | const value = breakpointsConfig[screen]; 25 | const rule = ` 26 | @media (min-width: ${breakpoint}) { 27 | padding-top: ${value}px; 28 | } 29 | `; 30 | 31 | rules.push(rule); 32 | } 33 | } 34 | 35 | return css` 36 | ${rules.join(' ')} 37 | `; 38 | }; 39 | 40 | const Spacing = styled.span` 41 | display: block; 42 | 43 | ${({ theme, size }) => 44 | size && 45 | (typeof size === 'number' 46 | ? setSpacing(size) 47 | : setBreakpointsSpacing(size, theme))} 48 | `; 49 | 50 | export default Spacing; 51 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from 'styled-components'; 2 | 3 | const theme: DefaultTheme = { 4 | colors: { 5 | primary: '#7C41FF', 6 | success: '#00D775', 7 | white: '#FFF', 8 | danger: '#FF4C63', 9 | gray: { 10 | 100: '#F5F6F8', 11 | 200: '#D7DCE1', 12 | 300: '#ACB5BF', 13 | 400: '#263238', 14 | 500: '#131415', 15 | }, 16 | }, 17 | screens: { 18 | xs: '320px', 19 | sm: '600px', 20 | md: '960px', 21 | lg: '1280px', 22 | xl: '1920px', 23 | }, 24 | fontSizes: { 25 | xs: { size: '0.75rem', lineHeight: '1.25rem' }, 26 | sm: { size: '0.875rem', lineHeight: '1.375rem' }, 27 | base: { size: '1rem', lineHeight: '1.5rem' }, 28 | lg: { size: '1.25rem', lineHeight: '1.75rem' }, 29 | xl: { size: '1.5rem', lineHeight: '2rem' }, 30 | '2xl': { size: '1.875rem', lineHeight: '2.375rem' }, 31 | '3xl': { size: '2.375rem', lineHeight: '2.875rem' }, 32 | '4xl': { size: '2.875rem', lineHeight: '3.375rem' }, 33 | '5xl': { size: '3.5rem', lineHeight: '4rem' }, 34 | }, 35 | borderRadius: { 36 | base: '12px', 37 | }, 38 | fontFamily: { 39 | sans: 'Inter, sans-serif', 40 | }, 41 | }; 42 | 43 | export * from './global-styles'; 44 | 45 | export default theme; 46 | -------------------------------------------------------------------------------- /src/components/global/Icon/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { DefaultTheme } from 'styled-components'; 2 | import { IconProps } from './create-icon'; 3 | import _get from 'lodash.get'; 4 | 5 | const sizes = { 6 | xs: 0.75, 7 | sm: 0.875, 8 | '1x': 1, 9 | '2x': 2, 10 | '3x': 3, 11 | '4x': 4, 12 | }; 13 | 14 | const setColorScheme = ( 15 | colorScheme: IconProps['colorScheme'], 16 | theme: DefaultTheme 17 | ) => { 18 | const { colors } = theme; 19 | const colorsScheme = { 20 | primary: colors.primary, 21 | danger: colors.danger, 22 | white: colors.white, 23 | }; 24 | 25 | const color = colorsScheme[colorScheme]; 26 | 27 | if (color) return `color: ${color};`; 28 | return ''; 29 | }; 30 | 31 | export const Svg = styled.svg` 32 | fill: currentColor; 33 | width: 1em; 34 | height: 1em; 35 | font-size: 1rem; 36 | 37 | & path { 38 | color: currentColor; 39 | } 40 | 41 | ${({ size }) => 42 | size && 43 | ` 44 | font-size: ${typeof size === 'number' ? `${size}px` : `${sizes[size]}rem`}; 45 | `} 46 | 47 | ${({ colorScheme, theme }) => 48 | colorScheme && setColorScheme(colorScheme, theme)} 49 | 50 | ${({ color, theme }) => 51 | color && 52 | ` 53 | color: ${_get(theme.colors, color)}; 54 | `} 55 | `; 56 | -------------------------------------------------------------------------------- /src/modules/home/Editing/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Tabs, { Tab } from '@components/Navigation/Tabs'; 3 | import LinksTab from '@localComponents/home/editing/LinksTab'; 4 | import AppearanceTab from '@localComponents/home/editing/AppearanceTab'; 5 | import SettingsTab from '@localComponents/home/editing/SettingsTab'; 6 | import { EditingRoot, TabsBox, ContentBox } from './styled'; 7 | 8 | const Editing = () => { 9 | const [tabActive, setTabActive] = useState('links'); 10 | 11 | return ( 12 | 13 | 14 | setTabActive(value as string)} 19 | > 20 | Links 21 | Apariencia 22 | Configuracion 23 | 24 | 25 | 26 |

27 | {tabActive === 'links' && } 28 | {tabActive === 'appearance' && } 29 | {tabActive === 'settings' && } 30 |
31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Editing; 37 | -------------------------------------------------------------------------------- /src/components/global/Form/Switch/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { SwitchProps } from './'; 3 | 4 | export const SwitchRoot = styled.div< 5 | Pick 6 | >` 7 | display: inline-block; 8 | background-color: ${({ theme }) => theme.colors.gray[300]}; 9 | height: 20px; 10 | width: 32px; 11 | border-radius: ${({ theme }) => theme.borderRadius.base}; 12 | position: relative; 13 | cursor: pointer; 14 | 15 | ${({ isEnabled, colorScheme, theme }) => 16 | isEnabled && 17 | colorScheme === 'primary' && 18 | ` 19 | background-color: ${theme.colors.primary}; 20 | `} 21 | 22 | ${({ isEnabled, colorScheme, theme }) => 23 | isEnabled && 24 | colorScheme === 'success' && 25 | ` 26 | background-color: ${theme.colors.success}; 27 | `} 28 | 29 | ${({ isEnabled }) => 30 | isEnabled && 31 | ` 32 | & > span { 33 | transform: translateY(-50%) translateX(calc(2px)); 34 | } 35 | `} 36 | `; 37 | 38 | export const Indicator = styled.span` 39 | height: 16px; 40 | width: 16px; 41 | background-color: ${({ theme }) => theme.colors.white}; 42 | position: absolute; 43 | top: 50%; 44 | transform: translateY(-50%) translateX(calc(100% - 2px)); 45 | border-radius: 50%; 46 | transition-property: transform; 47 | transition-duration: 250ms; 48 | `; 49 | -------------------------------------------------------------------------------- /src/components/local/home/editing/AppearanceTab/Themes/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Title from '@components/DataDisplay/Title'; 3 | import Text from '@components/DataDisplay/Text'; 4 | import Spacing from '@components/Layout/Spacing'; 5 | import { AppContext } from '@contexts/GlobalApp'; 6 | import { ContentBox, ThemesGrid, ThemeMediabox } from './styled'; 7 | 8 | const Themes = () => { 9 | const { state, dispatch } = useContext(AppContext); 10 | const { list: themes, selected: selectedTheme } = state.appearance.themes; 11 | 12 | return ( 13 |
14 | 15 | Temas 16 | 17 | 18 | 19 | 20 | {themes.map((theme) => ( 21 |
  • dispatch({ type: 'update:theme', payload: theme })} 24 | > 25 | 31 | {theme.name} 32 |
  • 33 | ))} 34 |
    35 |
    36 |
    37 | ); 38 | }; 39 | 40 | export default Themes; 41 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "jest": true 7 | }, 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": ["@typescript-eslint", "react"], 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:react/recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:prettier/recommended", 22 | "prettier" 23 | ], 24 | "settings": { 25 | "react": { 26 | "version": "detect" 27 | } 28 | }, 29 | "rules": { 30 | "@typescript-eslint/no-unused-vars": "off", 31 | "@typescript-eslint/no-explicit-any": "off", 32 | "@typescript-eslint/ban-ts-comment": "warn", 33 | "@typescript-eslint/explicit-module-boundary-types": "off", 34 | "quotes": ["error", "single"], 35 | "semi": "error", 36 | "react/prop-types": "off", 37 | "jsx-quotes": ["error", "prefer-single"], 38 | "no-unused-vars": "off", 39 | "no-console": [ 40 | "error", 41 | { 42 | "allow": ["warn", "error"] 43 | } 44 | ], 45 | "react/self-closing-comp": [ 46 | "error", 47 | { 48 | "component": true, 49 | "html": true 50 | } 51 | ], 52 | "prettier/prettier": [ 53 | "error", 54 | { 55 | "endOfLine": "lf" 56 | } 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/local/home/editing/AppearanceTab/Themes/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const ContentBox = styled.div` 4 | background-color: ${({ theme }) => theme.colors.white}; 5 | border-radius: ${({ theme }) => theme.borderRadius.base}; 6 | padding: 20px; 7 | box-shadow: 0px 4px 4px ${({ theme }) => theme.colors.gray[200]}; 8 | `; 9 | 10 | export const ThemesGrid = styled.ul` 11 | display: grid; 12 | grid-template-columns: repeat(2, minmax(0, 1fr)); 13 | gap: 26px; 14 | text-align: center; 15 | 16 | li { 17 | cursor: pointer; 18 | } 19 | 20 | @media screen and (min-width: ${({ theme }) => theme.screens.sm}) { 21 | grid-template-columns: repeat(3, minmax(0, 1fr)); 22 | } 23 | @media screen and (min-width: ${({ theme }) => theme.screens.lg}) { 24 | grid-template-columns: repeat(4, minmax(0, 1fr)); 25 | } 26 | `; 27 | 28 | export const ThemeMediabox = styled.div<{ src?: string; isActive?: boolean }>` 29 | padding-top: 150%; 30 | border-radius: ${({ theme }) => theme.borderRadius.base}; 31 | background-color: ${({ theme }) => theme.colors.gray[200]}; 32 | background-size: cover; 33 | background-position: center; 34 | margin-bottom: 4px; 35 | transition-property: box-shadow; 36 | transition-duration: 250ms; 37 | 38 | ${({ src }) => 39 | src && 40 | ` 41 | background-image: url(${src}); 42 | `} 43 | 44 | ${({ isActive, theme }) => 45 | isActive && 46 | ` 47 | box-shadow: 0 0 0 4px ${theme.colors.primary}; 48 | `} 49 | `; 50 | -------------------------------------------------------------------------------- /src/components/global/Icon/InstagramOutlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createIcon from './create-icon'; 3 | 4 | const InstagramOutlined = createIcon( 5 | 6 | ); 7 | 8 | export default InstagramOutlined; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linktree-react-clone", 3 | "version": "1.0.0", 4 | "description": "clone react linktree", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepare": "husky install", 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "eslint ./src --ext .ts --ext .tsx --ext .js", 12 | "lint:fix": "eslint ./src --ext .ts --ext .tsx --ext .js --fix", 13 | "format": "prettier --write ." 14 | }, 15 | "keywords": [], 16 | "author": "alexGarrixen", 17 | "license": "ISC", 18 | "dependencies": { 19 | "commitizen": "^4.2.4", 20 | "cz-conventional-changelog": "^3.3.0", 21 | "lodash.get": "^4.4.2", 22 | "next": "^11.0.1", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "shortid": "^2.2.16", 26 | "styled-components": "^5.3.0" 27 | }, 28 | "devDependencies": { 29 | "@commitlint/cli": "^12.1.4", 30 | "@commitlint/config-conventional": "^12.1.4", 31 | "@types/node": "^15.12.5", 32 | "@types/react": "^17.0.11", 33 | "@types/styled-components": "^5.1.10", 34 | "@typescript-eslint/eslint-plugin": "^4.28.1", 35 | "@typescript-eslint/parser": "^4.28.1", 36 | "babel-plugin-styled-components": "^1.13.1", 37 | "eslint": "^7.29.0", 38 | "eslint-config-next": "^11.0.1", 39 | "eslint-config-prettier": "^8.3.0", 40 | "eslint-plugin-prettier": "^3.4.0", 41 | "eslint-plugin-react": "^7.24.0", 42 | "husky": "^6.0.0", 43 | "prettier": "^2.3.2", 44 | "typescript": "^4.3.4" 45 | }, 46 | "config": { 47 | "commitizen": { 48 | "path": "./node_modules/cz-conventional-changelog" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/contexts/GlobalApp/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface Item { 4 | id: string; 5 | type: 'network'; 6 | props: { title: string; url: string; enabled: boolean }; 7 | } 8 | 9 | export interface Theme { 10 | id: string; 11 | name: string; 12 | src: string; 13 | } 14 | 15 | export interface State { 16 | appearance: { 17 | profile: { 18 | avatar: string | undefined; 19 | username: string; 20 | description: string; 21 | }; 22 | themes: { 23 | selected: Theme | null; 24 | list: Theme[]; 25 | }; 26 | }; 27 | config: { 28 | social: { 29 | facebook: string; 30 | instagram: string; 31 | twitter: string; 32 | }; 33 | }; 34 | items: Item[]; 35 | } 36 | 37 | export interface ContextValue { 38 | state: State; 39 | dispatch: React.Dispatch; 40 | } 41 | 42 | export type ActionTypes = 43 | | { 44 | type: 'update:profile'; 45 | payload: Partial; 46 | } 47 | | { 48 | type: 'update:theme'; 49 | payload: Theme; 50 | } 51 | | { 52 | type: 'delete:theme'; 53 | payload: { 54 | name: string; 55 | }; 56 | } 57 | | { 58 | type: 'update:social-networks'; 59 | payload: Partial; 60 | } 61 | | { 62 | type: 'add:item'; 63 | payload: { type: Item['type'] }; 64 | } 65 | | { 66 | type: 'delete:item'; 67 | payload: { 68 | index: number; 69 | }; 70 | } 71 | | { 72 | type: 'update:item'; 73 | payload: { 74 | index: number; 75 | data: Partial; 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/global/Icon/TwitterOutlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createIcon from './create-icon'; 3 | 4 | const TwitterOutlined = createIcon( 5 | 6 | ); 7 | 8 | export default TwitterOutlined; 9 | -------------------------------------------------------------------------------- /src/components/global/Navigation/Tabs/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { DefaultTheme } from 'styled-components'; 2 | import { TabsProps } from './'; 3 | 4 | const colorSchemeActive = (theme: DefaultTheme) => ({ 5 | primary: ` 6 | color: ${theme.colors.primary}; 7 | box-shadow: 0 -2px 0 0 ${theme.colors.primary} inset; 8 | &:hover { 9 | box-shadow: 0 -2px 0 0 ${theme.colors.primary} inset; 10 | } 11 | `, 12 | secondary: ` 13 | color: ${theme.colors.gray[400]}; 14 | box-shadow: 0 -2px 0 0 ${theme.colors.gray[400]} inset; 15 | &:hover { 16 | box-shadow: 0 -2px 0 0 ${theme.colors.gray[400]} inset; 17 | } 18 | `, 19 | }); 20 | 21 | export const TabsRoot = styled.div` 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | `; 26 | 27 | export const TabRoot = styled.li< 28 | Pick & { isActive?: boolean } 29 | >` 30 | padding: 12px 16px; 31 | font-size: ${({ theme }) => theme.fontSizes.base.size}; 32 | flex-shrink: 0; 33 | cursor: pointer; 34 | font-weight: 500; 35 | color: ${({ theme }) => theme.colors.gray[400]}; 36 | transition-property: box-shadow; 37 | transition-duration: 250ms; 38 | 39 | &:hover { 40 | box-shadow: 0 -2px 0 0 ${({ theme }) => theme.colors.gray[200]} inset; 41 | } 42 | 43 | ${({ size }) => 44 | size === 'sm' && 45 | ` 46 | padding-top: 6px; 47 | padding-bottom: 6px; 48 | `} 49 | 50 | ${({ size }) => 51 | size === 'lg' && 52 | ` 53 | padding-top: 20px; 54 | padding-bottom: 20px; 55 | `} 56 | 57 | ${({ isActive, colorScheme, theme }) => 58 | isActive && colorSchemeActive(theme)[colorScheme]}; 59 | `; 60 | 61 | export const TabsBox = styled.ul` 62 | display: inherit; 63 | overflow-x: auto; 64 | `; 65 | -------------------------------------------------------------------------------- /src/components/global/Icon/TrashOutlined.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createIcon from './create-icon'; 3 | 4 | const TrashOutlined = createIcon( 5 | 6 | ); 7 | 8 | export default TrashOutlined; 9 | -------------------------------------------------------------------------------- /src/components/global/Navigation/Tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TabRoot, TabsBox, TabsRoot } from './styled'; 3 | 4 | export type TabsProps = { 5 | children?: React.ReactNode; 6 | onChangeTab?: (value: string | number, ev: React.MouseEvent) => void; 7 | size?: 'sm' | 'md' | 'lg'; 8 | value?: number | string; 9 | colorScheme?: 'primary' | 'secondary'; 10 | className?: string; 11 | }; 12 | 13 | export type TabProps = { 14 | isActive?: boolean; 15 | children?: React.ReactNode; 16 | } & Pick< 17 | TabsProps, 18 | 'colorScheme' | 'onChangeTab' | 'size' | 'value' | 'className' 19 | >; 20 | 21 | const Tabs = ({ 22 | children: childrenProp, 23 | onChangeTab, 24 | value, 25 | colorScheme = 'secondary', 26 | size, 27 | ...rest 28 | }: TabsProps) => { 29 | let defaultValue = 0; 30 | 31 | const children = React.Children.map(childrenProp, (child: JSX.Element) => { 32 | const childValue = child.props.value || defaultValue; 33 | const isActive = childValue === value; 34 | defaultValue++; 35 | 36 | return React.cloneElement(child, { 37 | onChangeTab, 38 | value: childValue, 39 | isActive, 40 | colorScheme, 41 | size, 42 | }); 43 | }); 44 | 45 | return ( 46 | 47 | {children} 48 | 49 | ); 50 | }; 51 | 52 | export default Tabs; 53 | 54 | export const Tab = ({ 55 | value, 56 | onChangeTab, 57 | isActive, 58 | colorScheme, 59 | children, 60 | size, 61 | ...rest 62 | }: TabProps) => { 63 | const handleClick = React.useCallback( 64 | (e: React.MouseEvent) => { 65 | if (typeof onChangeTab === 'function') onChangeTab(value, e); 66 | }, 67 | [onChangeTab, value] 68 | ); 69 | 70 | return ( 71 | 78 | {children} 79 | 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/theme/global-styles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | *, 5 | *::before, 6 | *::after { 7 | box-sizing: border-box; 8 | outline: none; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | font-family: ${({ theme }) => theme.fontFamily.sans}; 15 | } 16 | 17 | h1, h2, h3, h4, h5, h6, p, a { 18 | margin: 0; 19 | } 20 | 21 | h1 { 22 | font-size: ${({ theme }) => theme.fontSizes['4xl'].size}; 23 | line-height: ${({ theme }) => theme.fontSizes['4xl'].lineHeight}; 24 | } 25 | 26 | h2 { 27 | font-size: ${({ theme }) => theme.fontSizes['3xl'].size}; 28 | line-height: ${({ theme }) => theme.fontSizes['3xl'].lineHeight}; 29 | } 30 | 31 | h3 { 32 | font-size: ${({ theme }) => theme.fontSizes['2xl'].size}; 33 | line-height: ${({ theme }) => theme.fontSizes['2xl'].lineHeight}; 34 | } 35 | 36 | h4 { 37 | font-size: ${({ theme }) => theme.fontSizes.xl.size}; 38 | line-height: ${({ theme }) => theme.fontSizes.xl.lineHeight}; 39 | } 40 | 41 | h5 { 42 | font-size: ${({ theme }) => theme.fontSizes.lg.size}; 43 | line-height: ${({ theme }) => theme.fontSizes.lg.lineHeight}; 44 | } 45 | 46 | h6 { 47 | font-size: ${({ theme }) => theme.fontSizes.base.size}; 48 | line-height: ${({ theme }) => theme.fontSizes.base.lineHeight}; 49 | } 50 | 51 | p { 52 | font-size: ${({ theme }) => theme.fontSizes.base.size}; 53 | line-height: ${({ theme }) => theme.fontSizes.base.lineHeight}; 54 | } 55 | 56 | ul { 57 | margin: 0; 58 | padding-left: 0; 59 | } 60 | 61 | li, ol { 62 | list-style: none; 63 | } 64 | 65 | a { 66 | text-decoration: none; 67 | cursor: pointer; 68 | } 69 | 70 | button, 71 | input, 72 | optgroup, 73 | select, 74 | textarea { 75 | font-family: inherit; 76 | } 77 | 78 | button { 79 | border: none; 80 | cursor: pointer; 81 | padding: 0; 82 | } 83 | `; 84 | -------------------------------------------------------------------------------- /src/components/local/home/editing/ItemCard/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { ItemCardProps } from '.'; 3 | 4 | export const ItemCardRoot = styled.article` 5 | background-color: ${({ theme }) => theme.colors.white}; 6 | box-shadow: 0px 4px 4px ${({ theme }) => theme.colors.gray[200]}; 7 | border-radius: ${({ theme }) => theme.borderRadius.base}; 8 | `; 9 | 10 | export const BodyBox = styled.div` 11 | display: flex; 12 | min-height: 120px; 13 | 14 | & > div:first-child { 15 | border-right: 1px solid ${({ theme }) => theme.colors.gray[200]}; 16 | width: 40px; 17 | } 18 | 19 | & > div:last-child { 20 | flex: 1; 21 | padding: 12px; 22 | display: flex; 23 | } 24 | `; 25 | 26 | export const ContentBox = styled.div< 27 | Pick 28 | >` 29 | flex: 1; 30 | display: grid; 31 | gap: 10px; 32 | 33 | ${({ centerAxisY }) => 34 | centerAxisY && 35 | ` 36 | align-items: center; 37 | align-content: center; 38 | `} 39 | 40 | ${({ centerAxisX }) => centerAxisX && 'justify-content: center;'} 41 | `; 42 | 43 | export const ActionsBox = styled.div` 44 | display: flex; 45 | flex-direction: column; 46 | justify-content: space-between; 47 | `; 48 | 49 | export const InputEditableBox = styled.div` 50 | display: flex; 51 | align-items: center; 52 | 53 | input { 54 | color: ${({ theme }) => theme.colors.gray[400]}; 55 | font-weight: 600; 56 | width: 100%; 57 | max-width: 70%; 58 | border: none; 59 | } 60 | 61 | button { 62 | background-color: transparent; 63 | margin-right: 3px; 64 | font-weight: 700; 65 | color: ${({ theme }) => theme.colors.gray[400]}; 66 | } 67 | `; 68 | 69 | export const DeleteBox = styled.div` 70 | text-align: center; 71 | 72 | > p { 73 | background-color: ${({ theme }) => theme.colors.gray[100]}; 74 | padding: 7px 4px; 75 | } 76 | 77 | > div { 78 | padding: 16px; 79 | display: grid; 80 | grid-template-columns: repeat(2, 1fr); 81 | gap: 16px; 82 | } 83 | `; 84 | -------------------------------------------------------------------------------- /src/components/global/DataDisplay/TypographyBase/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { DefaultTheme } from 'styled-components'; 2 | import _get from 'lodash.get'; 3 | import { TypographyProps } from '.'; 4 | 5 | const setSize = ( 6 | size: keyof DefaultTheme['fontSizes'], 7 | theme: DefaultTheme 8 | ) => { 9 | const fontSizes = theme.fontSizes; 10 | const fontSize = fontSizes[size]; 11 | 12 | if (fontSize) 13 | return ` 14 | font-size: ${fontSize.size}; 15 | line-height: ${fontSize.lineHeight}; 16 | `; 17 | 18 | return ''; 19 | }; 20 | 21 | const setBreakpointsSize = ( 22 | breakpoints: Partial< 23 | Record 24 | >, 25 | theme: DefaultTheme 26 | ) => { 27 | const { screens } = theme; 28 | const fontSizes = theme.fontSizes; 29 | const rules: string[] = []; 30 | 31 | for (const breakpoint of Object.keys(breakpoints)) { 32 | const screen = screens[breakpoint]; 33 | const fontSize = fontSizes[breakpoints[breakpoint]]; 34 | 35 | if (screen && fontSize) { 36 | rules.push(` 37 | @media screen and (min-width: ${screen}) { 38 | font-size: ${fontSize.size}; 39 | line-height: ${fontSize.lineHeight}; 40 | } 41 | `); 42 | } 43 | } 44 | 45 | return rules.join(' '); 46 | }; 47 | 48 | const setColorScheme = ( 49 | colorScheme: TypographyProps['colorScheme'], 50 | theme: DefaultTheme 51 | ) => { 52 | const { colors } = theme; 53 | const colorsMap = { 54 | primary: colors.primary, 55 | secondary: colors.gray[400], 56 | danger: colors.danger, 57 | }; 58 | const color = colorsMap[colorScheme]; 59 | 60 | if (color) 61 | return ` 62 | color: ${color}; 63 | `; 64 | 65 | return ''; 66 | }; 67 | 68 | export const TypographyRoot = styled.p` 69 | ${({ size, theme }) => 70 | size !== undefined 71 | ? typeof size === 'string' 72 | ? setSize(size, theme) 73 | : setBreakpointsSize(size, theme) 74 | : ''} 75 | 76 | ${({ colorScheme, theme }) => 77 | colorScheme && setColorScheme(colorScheme, theme)} 78 | 79 | ${({ color, theme }) => 80 | color && 81 | ` 82 | color: ${_get(theme.colors, color) || color}; 83 | `} 84 | `; 85 | -------------------------------------------------------------------------------- /src/contexts/GlobalApp/reducer.ts: -------------------------------------------------------------------------------- 1 | import shortid from 'shortid'; 2 | import { State, ActionTypes } from './types'; 3 | 4 | const reducer = (state: State, action: ActionTypes) => { 5 | switch (action.type) { 6 | case 'update:profile': { 7 | const values = action.payload; 8 | const clonedState = { ...state }; 9 | 10 | for (const prop of Object.keys(values)) { 11 | const value = values[prop]; 12 | clonedState.appearance.profile[prop] = value; 13 | } 14 | 15 | return clonedState; 16 | } 17 | case 'update:theme': { 18 | const theme = action.payload; 19 | const clonedState = { ...state }; 20 | 21 | clonedState.appearance.themes.selected = theme; 22 | return clonedState; 23 | } 24 | case 'delete:theme': { 25 | const themeName = action.payload.name; 26 | const clonedState = { ...state }; 27 | const themes = clonedState.appearance.themes.list; 28 | 29 | clonedState.appearance.themes.list = themes.filter( 30 | (theme) => theme.name !== themeName 31 | ); 32 | 33 | return clonedState; 34 | } 35 | case 'update:social-networks': { 36 | const values = action.payload; 37 | const clonedState = { ...state }; 38 | 39 | for (const prop of Object.keys(values)) { 40 | const value = values[prop]; 41 | clonedState.config.social[prop] = value; 42 | } 43 | 44 | return clonedState; 45 | } 46 | case 'add:item': { 47 | const typeItem = action.payload.type; 48 | const clonedState = { ...state }; 49 | const item = { 50 | id: shortid.generate(), 51 | type: typeItem, 52 | props: { enabled: true, title: 'Title', url: '' }, 53 | }; 54 | 55 | clonedState.items.push(item); 56 | return clonedState; 57 | } 58 | case 'delete:item': { 59 | const indexItem = action.payload.index; 60 | const clonedState = { ...state }; 61 | clonedState.items.splice(indexItem, 1); 62 | 63 | return clonedState; 64 | } 65 | case 'update:item': { 66 | const { index: indexItem, data: values } = action.payload; 67 | const clonedState = { ...state }; 68 | 69 | for (const prop of Object.keys(values)) { 70 | const value = values[prop]; 71 | clonedState.items[indexItem].props = { 72 | ...clonedState.items[indexItem].props, 73 | [prop]: value, 74 | }; 75 | } 76 | 77 | return clonedState; 78 | } 79 | default: 80 | return state; 81 | } 82 | }; 83 | 84 | export default reducer; 85 | -------------------------------------------------------------------------------- /src/components/local/home/editing/SettingsTab/SocialLinks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext } from 'react'; 2 | import styled from 'styled-components'; 3 | import Title from '@components/DataDisplay/Title'; 4 | import Input from '@components/Form/Input'; 5 | import Spacing from '@components/Layout/Spacing'; 6 | import { AppContext } from '@contexts/GlobalApp'; 7 | 8 | const ContentBox = styled.div` 9 | background-color: ${({ theme }) => theme.colors.white}; 10 | border-radius: ${({ theme }) => theme.borderRadius.base}; 11 | padding: 20px; 12 | margin-top: 30px; 13 | box-shadow: 0px 4px 4px ${({ theme }) => theme.colors.gray[200]}; 14 | 15 | & > div { 16 | display: grid; 17 | gap: 26px; 18 | } 19 | `; 20 | 21 | const SocialLinks = () => { 22 | const { state, dispatch } = useContext(AppContext); 23 | const networks = state.config.social; 24 | 25 | const handleChangeInput = useCallback( 26 | (ev: React.ChangeEvent) => { 27 | const { name, value } = ev.target; 28 | dispatch({ type: 'update:social-networks', payload: { [name]: value } }); 29 | }, 30 | [networks] 31 | ); 32 | 33 | return ( 34 |
    35 | 36 | Redes Sociales 37 | 38 | 39 |
    40 |
    41 | Facebook 42 | 43 | 50 |
    51 |
    52 | Twitter 53 | 54 | 61 |
    62 |
    63 | Instagram 64 | 65 | 72 |
    73 |
    74 |
    75 |
    76 | ); 77 | }; 78 | 79 | export default SocialLinks; 80 | -------------------------------------------------------------------------------- /src/components/global/Form/ButtonBase/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import _get from 'lodash.get'; 3 | import { ButtonBaseProps } from './'; 4 | 5 | export const ButtonBaseRoot = styled.button` 6 | border: none; 7 | font-weight: 600; 8 | border-radius: ${({ theme }) => theme.borderRadius.base}; 9 | 10 | ${({ colorScheme, variant, theme }) => 11 | colorScheme === 'primary' && 12 | variant === 'filled' && 13 | ` 14 | background-color: ${theme.colors.primary}; 15 | color: ${theme.colors.white}; 16 | `} 17 | 18 | ${({ colorScheme, variant, theme }) => 19 | colorScheme === 'primary' && 20 | variant === 'outlined' && 21 | ` 22 | background-color: transparent; 23 | box-shadow: 0 0 0 1px ${theme.colors.primary} inset; 24 | color: ${theme.colors.primary}; 25 | `} 26 | 27 | ${({ colorScheme, variant, theme }) => 28 | colorScheme === 'secondary' && 29 | variant === 'filled' && 30 | ` 31 | background-color: ${theme.colors.gray[500]}; 32 | color: ${theme.colors.white}; 33 | `} 34 | 35 | ${({ colorScheme, variant, theme }) => 36 | colorScheme === 'secondary' && 37 | variant === 'outlined' && 38 | ` 39 | background-color: transparent; 40 | box-shadow: 0 0 0 1px ${theme.colors.gray[500]} inset; 41 | color: ${theme.colors.gray[500]}; 42 | `} 43 | 44 | ${({ colorScheme, variant, theme }) => 45 | colorScheme === 'base' && 46 | variant === 'filled' && 47 | ` 48 | background-color: ${theme.colors.gray[100]}; 49 | color: ${theme.colors.gray[400]}; 50 | `} 51 | 52 | ${({ colorScheme, variant, theme }) => 53 | colorScheme === 'base' && 54 | variant === 'outlined' && 55 | ` 56 | background-color: transparent; 57 | box-shadow: 0 0 0 1px ${theme.colors.gray[100]} inset; 58 | color: ${theme.colors.gray[400]}; 59 | `} 60 | 61 | ${({ colorScheme, variant, theme }) => 62 | colorScheme === 'white' && 63 | variant === 'filled' && 64 | ` 65 | background-color: ${theme.colors.white}; 66 | color: ${theme.colors.gray[400]}; 67 | `} 68 | 69 | ${({ colorScheme, variant, theme }) => 70 | colorScheme === 'white' && 71 | variant === 'outlined' && 72 | ` 73 | background-color: transparent; 74 | box-shadow: 0 0 0 1px ${theme.colors.white} inset; 75 | color: ${theme.colors.white}; 76 | `} 77 | 78 | ${({ bgColor, theme }) => 79 | bgColor && 80 | ` 81 | background-color: ${_get(theme.colors, bgColor) || bgColor}; 82 | `} 83 | 84 | ${({ color, theme }) => 85 | color && 86 | ` 87 | color: ${_get(theme.colors, color) || color}; 88 | `} 89 | 90 | &:disabled { 91 | cursor: not-allowed; 92 | opacity: 0.4; 93 | } 94 | `; 95 | -------------------------------------------------------------------------------- /src/components/local/home/editing/LinksTab/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import styled from 'styled-components'; 3 | import Button from '@components/Form/Button'; 4 | import IconButton from '@components/Form/IconButton'; 5 | import Thunder from '@components/Icon/ThunderFilled'; 6 | import { AppContext } from '@contexts/GlobalApp'; 7 | import ItemCard, { Editable } from '../ItemCard'; 8 | 9 | const ActionsBox = styled.div` 10 | display: flex; 11 | align-items: stretch; 12 | gap: 12px; 13 | margin-bottom: 24px; 14 | 15 | button:first-child { 16 | flex: 1; 17 | } 18 | `; 19 | 20 | const ItemsGrid = styled.ul` 21 | display: grid; 22 | grid-gap: 24px; 23 | `; 24 | 25 | const LinksTab = () => { 26 | const { state, dispatch } = useContext(AppContext); 27 | const { items } = state; 28 | 29 | const handleCreateItem = () => 30 | dispatch({ 31 | type: 'add:item', 32 | payload: { type: 'network' }, 33 | }); 34 | 35 | const handleChangeEditable = 36 | (idx: number) => (ev: React.ChangeEvent) => { 37 | const { name, value } = ev.target; 38 | dispatch({ 39 | type: 'update:item', 40 | payload: { index: idx, data: { [name]: value } }, 41 | }); 42 | }; 43 | 44 | const handleChangeEnabled = (idx: number, isEnabled: boolean) => 45 | dispatch({ 46 | type: 'update:item', 47 | payload: { index: idx, data: { enabled: isEnabled } }, 48 | }); 49 | 50 | return ( 51 |
    52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | {items.map(({ id, props }, idx) => ( 62 |
  • 63 | handleChangeEnabled(idx, isEnabled)} 67 | onRequestRemove={() => 68 | dispatch({ type: 'delete:item', payload: { index: idx } }) 69 | } 70 | > 71 | 77 | 83 | 84 |
  • 85 | ))} 86 |
    87 |
    88 | ); 89 | }; 90 | 91 | export default LinksTab; 92 | -------------------------------------------------------------------------------- /src/components/local/home/previewLive/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import shortid from 'shortid'; 3 | import Avatar from '@components/Media/Avatar'; 4 | import Title from '@components/DataDisplay/Title'; 5 | import Text from '@components/DataDisplay/Text'; 6 | import Button from '@components/Form/Button'; 7 | import Facebook from '@components/Icon/FacebookOutlined'; 8 | import Instagram from '@components/Icon/InstagramOutlined'; 9 | import Twitter from '@components/Icon/TwitterOutlined'; 10 | import Spacing from '@components/Layout/Spacing'; 11 | import { AppContext } from '@contexts/GlobalApp'; 12 | import { PreviewRoot, ProfileBox, ItemsBox, SocialNetworksBox } from './styled'; 13 | 14 | const Preview = () => { 15 | const { state } = useContext(AppContext); 16 | const { profile } = state.appearance; 17 | const { social } = state.config; 18 | const { items } = state; 19 | const { list: themes, selected: theme } = state.appearance.themes; 20 | const fallbackTheme = themes[0]; 21 | 22 | return ( 23 | 24 |
    25 | 26 | 27 | 28 | 29 | {profile.username} 30 | 31 | 32 | {profile.description} 33 | 34 | 35 | 36 | 37 | {items.map( 38 | ({ props }) => 39 | props.enabled && ( 40 | 41 |
  • 42 | 45 |
  • 46 |
    47 | ) 48 | )} 49 |
    50 | 51 | 52 | {social.facebook && ( 53 | 54 |
  • 55 | 56 |
  • 57 |
    58 | )} 59 | {social.instagram && ( 60 | 61 |
  • 62 | 63 |
  • 64 |
    65 | )} 66 | {social.twitter && ( 67 | 68 |
  • 69 | 70 |
  • 71 |
    72 | )} 73 |
    74 |
    75 |
    76 | ); 77 | }; 78 | 79 | export default Preview; 80 | -------------------------------------------------------------------------------- /src/components/local/home/editing/AppearanceTab/Profile/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import Title from '@components/DataDisplay/Title'; 3 | import Avatar from '@components/Media/Avatar'; 4 | import Button from '@components/Form/Button'; 5 | import Input from '@components/Form/Input'; 6 | import Spacing from '@components/Layout/Spacing'; 7 | import { AppContext } from '@contexts/GlobalApp'; 8 | import { ContentBox, HeadingBox, InputsBox } from './styled'; 9 | 10 | const Profile = () => { 11 | const { state, dispatch } = useContext(AppContext); 12 | const profile = state.appearance.profile; 13 | 14 | const handleChangeInput = ( 15 | ev: React.ChangeEvent 16 | ) => { 17 | const { name, value } = ev.target; 18 | dispatch({ type: 'update:profile', payload: { [name]: value } }); 19 | }; 20 | 21 | const handlePickAvatar = (ev: React.ChangeEvent) => { 22 | const { files, name } = ev.target; 23 | 24 | if (files[0]) { 25 | const urlImg = URL.createObjectURL(files[0]); 26 | dispatch({ type: 'update:profile', payload: { [name]: urlImg } }); 27 | } 28 | }; 29 | 30 | return ( 31 |
    32 | 33 | Perfil 34 | 35 | 36 | 37 | 38 | 39 |
    40 | 45 | 53 | 61 |
    62 |
    63 | 64 | 65 |
    66 | Nombre de usuario 67 | 68 | 76 |
    77 |
    78 | Descripcion 79 | 80 | 89 |
    90 |
    91 |
    92 |
    93 | ); 94 | }; 95 | 96 | export default Profile; 97 | -------------------------------------------------------------------------------- /src/components/local/home/editing/ItemCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import Switch from '@components/Form/Switch'; 3 | import Button from '@components/Form/Button'; 4 | import Trash from '@components/Icon/TrashOutlined'; 5 | import Pencil from '@components/Icon/PencilOutlined'; 6 | import Text from '@components/DataDisplay/Text'; 7 | import { 8 | ItemCardRoot, 9 | BodyBox, 10 | ContentBox, 11 | ActionsBox, 12 | InputEditableBox, 13 | DeleteBox, 14 | } from './styled'; 15 | 16 | type EditableProps = { 17 | placeholder?: string; 18 | onChange?: (e: React.ChangeEvent) => void; 19 | value?: string; 20 | name?: string; 21 | }; 22 | 23 | export const Editable = ({ 24 | placeholder, 25 | value: valueProp, 26 | name, 27 | onChange, 28 | }: EditableProps) => { 29 | const [modeEditable, setModeEditable] = useState(false); 30 | const [value, setValue] = useState(valueProp || ''); 31 | const handleChange = useCallback( 32 | (ev: React.ChangeEvent) => { 33 | setValue(ev.target.value); 34 | onChange && onChange(ev); 35 | }, 36 | [value] 37 | ); 38 | 39 | return ( 40 | 41 | {modeEditable ? ( 42 | setModeEditable(false)} 50 | /> 51 | ) : ( 52 | <> 53 | 56 | 57 | 58 | )} 59 | 60 | ); 61 | }; 62 | 63 | export type ItemCardProps = { 64 | children?: React.ReactNode; 65 | centerAxisX?: boolean; 66 | centerAxisY?: boolean; 67 | onRequestRemove?: (ev?: React.MouseEvent) => void; 68 | onEnable?: (isEnabled: boolean) => void; 69 | isEnabled?: boolean; 70 | }; 71 | 72 | const ItemCard = ({ 73 | children, 74 | centerAxisX, 75 | centerAxisY, 76 | isEnabled, 77 | onRequestRemove, 78 | onEnable, 79 | }: ItemCardProps) => { 80 | const [showDelete, setShowDelete] = useState(false); 81 | 82 | return ( 83 | 84 | 85 |
    86 |
    87 | 88 | {children} 89 | 90 | 91 | 96 | setShowDelete(!showDelete)} 101 | /> 102 | 103 |
    104 | 105 | {showDelete && ( 106 | 107 | 108 | ¿Eliminar esto para siempre? 109 | 110 |
    111 | 112 | 115 |
    116 |
    117 | )} 118 | 119 | ); 120 | }; 121 | 122 | export default ItemCard; 123 | --------------------------------------------------------------------------------