@ -0,0 +1,48 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
amd: true,
|
||||
node: true,
|
||||
},
|
||||
plugins: ['prettier', 'simple-import-sort'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'simple-import-sort/sort': 'error',
|
||||
'sort-imports': 'off',
|
||||
'import/order': 'off',
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'error',
|
||||
{
|
||||
components: ['Link'],
|
||||
specialLink: ['hrefLeft', 'hrefRight'],
|
||||
aspects: ['invalidHref', 'preferButton'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
docker/
|
||||
.env
|
||||
dump*
|
||||
|
||||
.log
|
||||
|
||||
assets/
|
@ -0,0 +1,4 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
trailingComma: "all",
|
||||
singleQuote: true,
|
||||
printWidth: 90,
|
||||
tabWidth: 2
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
# Artimañas 2020
|
||||
|
||||
Este es el repositorio para el código del sitio web de Artimañas 2020.
|
||||
|
||||
## Instrucciones para desarrollo
|
||||
|
||||
La aplicación web consiste de dos partes:
|
||||
|
||||
1. CMS: [Directus](https://directus.io/)
|
||||
2. Componente de SSR (Server Side Rendering): [Next.js](https://nextjs.org/)
|
||||
|
||||
### Configurando el entorno
|
||||
|
||||
Hay 3 variables de entorno que se deben configurar en el archivo `.env`, la primera corresponde a la contraseña de la base de datos de MySQL y las otras dos a la seguridad de Directus:
|
||||
|
||||
- `MYSQL_PASSWORD`
|
||||
- `DIRECTUS_AUTH_PUBLICKEY`
|
||||
- `DIRECTUS_AUTH_SECRETKEY`
|
||||
|
||||
Estas pueden ser cualquier string, pero se deben asignar antes de crear los contenedores, y si se modifican posteriormente, los contenedores no funcionaran correctamente.
|
||||
|
||||
El script `setup-env.sh` permite generar valores para las variables de entorno automáticamente y crear e iniciar los contenedores de docker después de esto.
|
||||
|
||||
``` bash
|
||||
./setup-env.sh
|
||||
```
|
||||
|
||||
### CMS y base de datos
|
||||
|
||||
Directus depende de una base de datos SQL que se puede levantar usando [Docker](https://www.docker.com/) con el archivo de [docker-compose](https://docs.docker.com/compose/) provisto:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Cuando el contenedor de la base de datos se cree por primera vez, la base de datos se inicializará con el dump que se encuentra en `./db/init.sql`. Este contiene las tablas para las obras, biografías, e información general del sitio, así como los usuarios correspondientes a cada alumno de la materia. Para modificar los datos una vez iniciados los contenedores, se puede acceder a la interfaz web de directus en [http://localhost:8080](http://localhost:8080) con las siguientes credenciales:
|
||||
|
||||
- Usuario: `admin@artiweb.net`
|
||||
- Contraseña: `password`
|
||||
|
||||
Eventualmente, este dump deberá ser actualizado con el contenido real/final, para que el entorno de desarrollo sea lo mas fiel posible con respecto al de producción. Esto se puede llevar a cabo con el script provisto en la raíz del repositorio (`manage-db`):
|
||||
|
||||
``` bash
|
||||
./manage-db.sh backup
|
||||
```
|
||||
|
||||
### Sitio web (front end)
|
||||
|
||||
Una vez que la base de datos haya sido inicializada, se puede iniciar el servidor de desarrollo (componente de SSR) con:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Los archivos en el directorio `src` se pueden editar y los cambios se verán reflejados en el navegador sin la necesidad de recargar la página.
|
||||
|
||||
## Screencasts
|
||||
|
||||
- [Configurando el servidor de desarrollo en Manjaro / Arch Linux](https://youtu.be/1_Eo37owlDw)
|
||||
- [Descripción de los archivos del respositorio](https://youtu.be/5-D9CbGm-8Q)
|
@ -1,6 +1,7 @@
|
||||
MYSQL_HOST=mysql
|
||||
MYSQL_DB=directus
|
||||
MYSQL_PASSWORD=directus
|
||||
MYSQL_USER=directus
|
||||
MYSQL_PASSWORD=
|
||||
DIRECTUS_AUTH_PUBLICKEY=
|
||||
DIRECTUS_AUTH_SECRETKEY=
|
||||
|
||||
DIRECTUS_HOST=
|
||||
DIRECTUS_API_USER=
|
||||
DIRECTUS_API_PASSWORD=
|
||||
|
@ -0,0 +1,59 @@
|
||||
const dotenv = require('dotenv')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const { DIRECTUS_HOST, DIRECTUS_API_USER, DIRECTUS_API_PASSWORD } = process.env
|
||||
|
||||
async function main() {
|
||||
const res = await fetch(`${DIRECTUS_HOST}/_/auth/authenticate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: DIRECTUS_API_USER, password: DIRECTUS_API_PASSWORD }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
const { data } = await res.json()
|
||||
const { token } = data
|
||||
|
||||
const roles = await fetch(`${DIRECTUS_HOST}/_/roles?access_token=${token}`)
|
||||
const rolesJson = await roles.json()
|
||||
|
||||
const studentsRole = rolesJson.data.find((role) => role.name === 'Alumn@')
|
||||
const guestsRole = rolesJson.data.find((role) => role.name === 'Invitad@')
|
||||
const studentsRoleId = studentsRole.id
|
||||
const guestsRoleId = guestsRole.id
|
||||
|
||||
const allUsers = await fetch(`${DIRECTUS_HOST}/_/users?access_token=${token}`)
|
||||
const allUsersJson = await allUsers.json()
|
||||
const usersFiltered = allUsersJson.data.filter(
|
||||
(user) => user.role === studentsRoleId || user.role === guestsRoleId,
|
||||
)
|
||||
|
||||
const allEvents = await fetch(
|
||||
`${DIRECTUS_HOST}/_/items/cronograma?access_token=${token}`,
|
||||
)
|
||||
const allEventsJson = await allEvents.json()
|
||||
|
||||
const allEventsWithUserNames = allEventsJson.data.map((event) => {
|
||||
const user = usersFiltered.find((user) => user.id === event.user_asociado)
|
||||
|
||||
if (!user) return event
|
||||
|
||||
const user_name = `${user.first_name.split(/[ ,]+/)[0]} ${
|
||||
user.last_name.split(/[ ,]+/)[0]
|
||||
}`
|
||||
|
||||
return {
|
||||
...event,
|
||||
user_name,
|
||||
}
|
||||
})
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'src/events.json'),
|
||||
JSON.stringify(allEventsWithUserNames),
|
||||
)
|
||||
}
|
||||
|
||||
main()
|
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
|
||||
declare const Textfit: any
|
||||
declare module 'react-textfit'
|
@ -0,0 +1,42 @@
|
||||
const path = require('path')
|
||||
const withPlugins = require('next-compose-plugins')
|
||||
const withSourceMaps = require('@zeit/next-source-maps')
|
||||
const optimizedImages = require('next-optimized-images')
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
})
|
||||
// const withCSS = require('@zeit/next-css')
|
||||
|
||||
const basePath = process.env.NODE_ENV === 'development' ? '' : ''
|
||||
|
||||
module.exports = withPlugins(
|
||||
[
|
||||
[withBundleAnalyzer({})],
|
||||
[
|
||||
withSourceMaps({
|
||||
webpack(config) {
|
||||
return config
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
optimizedImages,
|
||||
{
|
||||
responsive: {
|
||||
adapter: require('responsive-loader/sharp'),
|
||||
publicPath: path.join(basePath, '_next/static/images/'),
|
||||
},
|
||||
},
|
||||
],
|
||||
// [withCSS({ cssModules: true })],
|
||||
],
|
||||
{
|
||||
publicRuntimeConfig: {
|
||||
basePath,
|
||||
},
|
||||
serverRuntimeConfig: {
|
||||
PROJECT_ROOT: __dirname,
|
||||
},
|
||||
basePath,
|
||||
},
|
||||
)
|
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "seminario-next",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 13001",
|
||||
"build": "next build",
|
||||
"export": "next export",
|
||||
"start": "next start",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.0.0",
|
||||
"@chakra-ui/theme": "^1.0.0",
|
||||
"@chakra-ui/theme-tools": "^1.0.0",
|
||||
"@directus/sdk-js": "^6.3.0",
|
||||
"@emotion/react": "^11.1.1",
|
||||
"@emotion/styled": "^11.0.0",
|
||||
"@next/bundle-analyzer": "^9.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.8.1",
|
||||
"@typescript-eslint/parser": "^4.8.1",
|
||||
"@zeit/next-css": "^1.0.1",
|
||||
"@zeit/next-source-maps": "0.0.3",
|
||||
"bent": "^7.3.12",
|
||||
"eslint": "^7.13.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"focus-visible": "^5.2.0",
|
||||
"framer-motion": "^2.9.4",
|
||||
"imagemin-mozjpeg": "^9.0.0",
|
||||
"imagemin-optipng": "^8.0.0",
|
||||
"lodash": "^4.17.20",
|
||||
"lqip-loader": "^2.2.1",
|
||||
"next": "^10.0.2",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-optimized-images": "^2.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"prettier": "^2.1.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-headroom": "^3.0.0",
|
||||
"react-icons": "^3.11.0",
|
||||
"react-markdown": "^5.0.3",
|
||||
"react-textfit": "^1.1.0",
|
||||
"responsive-loader": "^2.2.0",
|
||||
"sharp": "^0.26.3",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"typeface-ibm-plex-sans": "^1.1.13",
|
||||
"webp-loader": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bent": "^7.3.2",
|
||||
"@types/node": "^14.14.10",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "^16.9.56",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react-headroom": "^2.2.1",
|
||||
"@types/smoothscroll-polyfill": "^0.3.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^5.0.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"typescript": "^4.1.2"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 300 B |
After Width: | Height: | Size: 486 B |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 335 B |
@ -0,0 +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"}
|
@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
|
||||
check_available() {
|
||||
# Function to check if a program is installed
|
||||
which $1 &> /dev/null
|
||||
|
||||
if [ $? = 1 ]; then
|
||||
echo "$1 is not available, please install it before running the script"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
gen_password() {
|
||||
check_available openssl
|
||||
openssl rand -base64 32
|
||||
}
|
||||
|
||||
read -p "MySQL password: (press enter to randomize): " mysql_password
|
||||
mysql_password=${mysql_password:-`gen_password`}
|
||||
|
||||
read -p "Directus pubkey: (press enter to randomize): " directus_pubkey
|
||||
directus_pubkey=${directus_pubkey:-`gen_password`}
|
||||
|
||||
read -p "Directus secretkey: (press enter to randomize): " directus_secret
|
||||
directus_secret=${directus_secret:-`gen_password`}
|
||||
|
||||
if [ ! -f "./.env" ]; then
|
||||
echo "Copying ./env.example to ./.env"
|
||||
cp ./env.example ./.env
|
||||
fi
|
||||
|
||||
sed -i -e "s#MYSQL_PASSWORD=.*#MYSQL_PASSWORD=${mysql_password}#g" \
|
||||
"$(dirname "$0")/.env"
|
||||
|
||||
sed -i -e "s#DIRECTUS_AUTH_PUBLICKEY=.*#DIRECTUS_AUTH_PUBLICKEY=${directus_pubkey}#g" \
|
||||
"$(dirname "$0")/.env"
|
||||
|
||||
sed -i -e "s#DIRECTUS_AUTH_SECRETKEY=.*#DIRECTUS_AUTH_SECRETKEY=${directus_secret}#g" \
|
||||
"$(dirname "$0")/.env"
|
||||
|
||||
read -p "Start docker containers? (requires docker-compose) [Y/n] " start_docker
|
||||
start_docker=${start_docker:-Y}
|
||||
|
||||
if [[ $start_docker =~ [yY] ]]; then
|
||||
check_available docker-compose
|
||||
sudo docker-compose up -d
|
||||
|
||||
# while true; do
|
||||
# sudo docker-compose logs mysql | grep "mysqld: ready for connections" &> /dev/null
|
||||
# EC=$?
|
||||
# if [ $EC -eq 0 ]; then
|
||||
# sleep 5
|
||||
# sudo docker-compose run --rm directus install --email admin@artiweb.net --password password
|
||||
# break
|
||||
# fi
|
||||
# done
|
||||
fi
|
@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import { ChakraProps, Box } from '@chakra-ui/core'
|
||||
|
||||
import { ReactComponent as LogoSVG } from '../images/header/logo.svg'
|
||||
|
||||
interface LogoProps {
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const Logo: React.FC = (props) => {
|
||||
return <LogoSVG {...props} />
|
||||
}
|
||||
|
||||
const StyledLogo: React.FC<ChakraProps & LogoProps> = (props) => {
|
||||
return <Box as={Logo} {...props} />
|
||||
}
|
||||
|
||||
export default StyledLogo
|
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { IconButton } from '@chakra-ui/react'
|
||||
import { MdMenu, MdClose } from 'react-icons/md'
|
||||
|
||||
interface MenuButtonProps {
|
||||
isOpen?: boolean
|
||||
handleClick?: () => void
|
||||
}
|
||||
|
||||
const MenuButton: React.FC<MenuButtonProps> = ({ handleClick, isOpen = false }) => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={handleClick}
|
||||
aria-label="Open menu"
|
||||
icon={!isOpen ? <MdMenu /> : <MdClose />}
|
||||
fontSize="2rem"
|
||||
mr="1rem"
|
||||
variant="ghost"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuButton
|
@ -0,0 +1,86 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { AnimateSharedLayout, motion } from 'framer-motion'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Link as ChakraLink, Flex, Box, Stack, HStack } from '@chakra-ui/react'
|
||||
import Headroom from 'react-headroom'
|
||||
|
||||
import paths from '../paths'
|
||||
|
||||
const Container = motion.custom(Flex)
|
||||
|
||||
const containerVariants = {
|
||||
open: {
|
||||
opacity: 1,
|
||||
display: 'flex',
|
||||
},
|
||||
closed: {
|
||||
opacity: 0,
|
||||
transition: {
|
||||
when: 'afterChildren',
|
||||
},
|
||||
transitionEnd: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type NavProps = {
|
||||
handleClick: () => void
|
||||
variant?: string
|
||||
}
|
||||
|
||||
const Nav: React.FC<NavProps> = ({ handleClick, variant = undefined }) => {
|
||||
const { pathname } = useRouter()
|
||||
|
||||
return (
|
||||
<Container
|
||||
direction="row"
|
||||
wrap="wrap"
|
||||
h="100%"
|
||||
justify="center"
|
||||
align="center"
|
||||
display="flex"
|
||||
variants={containerVariants}
|
||||
animate={variant}
|
||||
>
|
||||
<Stack
|
||||
direction={['column', 'row']}
|
||||
spacing={['1rem', '2.5rem']}
|
||||
position={['absolute', 'static']}
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
{paths.map((path) => {
|
||||
const currentLocation = pathname === '/' ? '/' : pathname.substring(1)
|
||||
const currentPath = path.path === '/' ? '/' : path.path.substring(1)
|
||||
|
||||
if (currentPath === '/') {
|
||||
return null
|
||||
}
|
||||
|
||||
const current =
|
||||
currentLocation === currentPath ||
|
||||
(currentPath !== '/' && currentLocation.includes(currentPath))
|
||||
|
||||
return (
|
||||
<Box key={path.path}>
|
||||
<Link href={path.path} passHref>
|
||||
<ChakraLink
|
||||
onClick={handleClick}
|
||||
fontWeight="bold"
|
||||
color={current ? 'blue' : 'black'}
|
||||
fontSize="md"
|
||||
>
|
||||
{path.name}
|
||||
</ChakraLink>
|
||||
</Link>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
@ -0,0 +1,105 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Image,
|
||||
Link as ChakraLink,
|
||||
Box,
|
||||
Flex,
|
||||
Spacer,
|
||||
useBreakpointValue,
|
||||
} from '@chakra-ui/react'
|
||||
import { motion } from 'framer-motion'
|
||||
import Headroom from 'react-headroom'
|
||||
|
||||
import Nav from './Nav'
|
||||
import MenuButton from './MenuButton'
|
||||
|
||||
export const closedNavHeight = '76px'
|
||||
|
||||
const headerVariants = {
|
||||
open: {
|
||||
height: '100vh',
|
||||
transition: {
|
||||
ease: 'easeInOut',
|
||||
duration: 0.3,
|
||||
when: 'beforeChildren',
|
||||
},
|
||||
},
|
||||
closed: {
|
||||
height: closedNavHeight,
|
||||
transition: {
|
||||
when: 'afterChildren',
|
||||
ease: 'easeInOut',
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const MotionFlex = motion.custom(Flex)
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
const isMobile = useBreakpointValue<boolean>({
|
||||
base: true,
|
||||
sm: true,
|
||||
md: true,
|
||||
lg: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty('--nav-height', closedNavHeight)
|
||||
})
|
||||
|
||||
const toggleNav = () => {
|
||||
setNavOpen(!navOpen)
|
||||
|
||||
const body = document.getElementsByTagName('body')[0]
|
||||
body.classList.toggle('lock-scroll')
|
||||
}
|
||||
|
||||
const closeNav = () => {
|
||||
setNavOpen(false)
|
||||
|
||||
const body = document.getElementsByTagName('body')[0]
|
||||
body.classList.remove('lock-scroll')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile && navOpen) {
|
||||
closeNav()
|
||||
}
|
||||
}, [isMobile, navOpen])
|
||||
|
||||
return (
|
||||
<Headroom style={{ zIndex: 2 }}>
|
||||
<MotionFlex
|
||||
layout="position"
|
||||
w="100%"
|
||||
direction="column"
|
||||
position="absolute"
|
||||
variants={headerVariants}
|
||||
initial="closed"
|
||||
animate={navOpen ? 'open' : 'closed'}
|
||||
bg="white"
|
||||
>
|
||||
<Flex h={closedNavHeight} align="center" px={[null, null, '2rem']}>
|
||||
<Link href="/" passHref>
|
||||
<ChakraLink onClick={() => closeNav()}>
|
||||
<Image src="/images/logo.svg" alt="Logo del estudio" p="1rem" h="100%" />
|
||||
</ChakraLink>
|
||||
</Link>
|
||||
<Spacer />
|
||||
{isMobile ? (
|
||||
<MenuButton isOpen={navOpen} handleClick={() => toggleNav()} />
|
||||
) : (
|
||||
<Nav variant="open" handleClick={() => closeNav()} />
|
||||
)}
|
||||
</Flex>
|
||||
{isMobile ? <Nav handleClick={() => closeNav()} /> : null}
|
||||
</MotionFlex>
|
||||
<Box w="100%" h={closedNavHeight} />
|
||||
</Headroom>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
@ -0,0 +1,73 @@
|
||||
import { Image, Box, BoxProps, SystemStyleObject } from '@chakra-ui/react'
|
||||
|
||||
type ResponsiveImageProps = {
|
||||
url: string | null
|
||||
avatar?: boolean
|
||||
alt: string
|
||||
imageStyle?: SystemStyleObject
|
||||
filter?: string
|
||||
}
|
||||
|
||||
// const defaultSizes = 'sizes[]=300,sizes[]=600,sizes[]=900,sizes[]=1200,sizes[]=1800'
|
||||
|
||||
const ResponsiveImage: React.FC<ResponsiveImageProps & BoxProps> = ({
|
||||
url,
|
||||
avatar,
|
||||
alt,
|
||||
children,
|
||||
imageStyle,
|
||||
w,
|
||||
h,
|
||||
...rest
|
||||
}) => {
|
||||
if (url === null) return null
|
||||
|
||||
let placeholder
|
||||
let responsiveImage
|
||||
let responsiveImageWebp
|
||||
|
||||
// The sizes are hardcoded because of a bug with webpack that impedes parametrization string interpolation
|
||||
// after the question mark
|
||||
// see https://github.com/cyrilwanner/next-optimized-images/issues/16
|
||||
if (avatar) {
|
||||
placeholder = require(`../assets/${url}?lqip`)
|
||||
responsiveImage = require(`../assets/${url}?resize&sizes[]=96,sizes[]=128,sizes[]=256&format=jpg`)
|
||||
responsiveImageWebp = require(`../assets/${url}?resize&sizes[]=96,sizes[]=128,sizes[]=256&format=webp`)
|
||||
} else {
|
||||
placeholder = require(`../assets/${url}?lqip`)
|
||||
responsiveImage = require(`../assets/${url}?resize&sizes[]=300,sizes[]=600,sizes[]=900,sizes[]=1200,sizes[]=1800&format=jpg`)
|
||||
responsiveImageWebp = require(`../assets/${url}?resize&sizes[]=300,sizes[]=600,sizes[]=900,sizes[]=1200,sizes[]=1800&format=webp`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
width={w ?? responsiveImage.width}
|
||||
height={h ?? responsiveImage.height}
|
||||
background={`url(${placeholder})`}
|
||||
backgroundSize="cover"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
objectFit="cover"
|
||||
sx={{
|
||||
'& picture img': imageStyle,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
<picture>
|
||||
<source srcSet={responsiveImageWebp.srcSet} type="image/webp" />
|
||||
<Image
|
||||
objectFit="cover"
|
||||
w="100%"
|
||||
h="100%"
|
||||
alt={alt}
|
||||
src={responsiveImage.src}
|
||||
srcSet={responsiveImage.srcSet}
|
||||
loading="lazy"
|
||||
/>
|
||||
</picture>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResponsiveImage
|
@ -0,0 +1,24 @@
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const defaultTitle = 'Estudio'
|
||||
|
||||
type SEOProps = {
|
||||
title?: string
|
||||
}
|
||||
|
||||
const SEO: React.FC<SEOProps> = ({ title }) => {
|
||||
const t = title ? `${title} | ${defaultTitle}` : defaultTitle
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{t}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta property="og:title" content={t} key="ogtitle" />
|
||||
<meta property="og:url" content={`${router.pathname}`} />
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
export default SEO
|
@ -0,0 +1,232 @@
|
||||
import DirectusSDK from '@directus/sdk-js'
|
||||
import { IFile } from '@directus/sdk-js/dist/types/schemes/directus/File'
|
||||
import bent from 'bent'
|
||||
import fs from 'fs'
|
||||
import getConfig from 'next/config'
|
||||
import path from 'path'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const { serverRuntimeConfig } = getConfig()
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
const getBuffer = bent('buffer')
|
||||
|
||||
const assetsDir = path.join(serverRuntimeConfig.PROJECT_ROOT, '/src/assets/')
|
||||
|
||||
const notasImagesDir = path.join(assetsDir, '/notas/')
|
||||
const workImagesDir = path.join(assetsDir, '/work/')
|
||||
const avatarImagesDir = path.join(assetsDir, '/avatar/')
|
||||
|
||||
const createDir = (dir: string) => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.info(`Creating directory ${dir}`)
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
createDir(assetsDir)
|
||||
createDir(notasImagesDir)
|
||||
createDir(workImagesDir)
|
||||
|
||||
export interface IFileWithData extends IFile {
|
||||
filename_disk: string
|
||||
data: {
|
||||
full_url: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function getImage(id: number) {
|
||||
const images = ((await client.getFiles()) as unknown) as { data: Array<IFileWithData> }
|
||||
return images.data.find((file) => file.id === id)
|
||||
}
|
||||
|
||||
export async function downloadFile(id: number, dir: string): Promise<string | null> {
|
||||
const imageData = await getImage(id)
|
||||
if (!imageData) return null
|
||||
const fileDir = path.join(dir, imageData.filename_disk)
|
||||
|
||||
if (!fs.existsSync(fileDir)) {
|
||||
console.info(`Downloading image with id ${id} as ${fileDir}`)
|
||||
const buffer = await getBuffer(imageData.data.full_url.replace('http', 'https'))
|
||||
await writeFile(fileDir, buffer as Buffer)
|
||||
console.info(`Succesfully downloaded image with ${fileDir}`)
|
||||
} else {
|
||||
console.info(`Image ${fileDir} exists. Skipping download`)
|
||||
}
|
||||
|
||||
return imageData.filename_disk
|
||||
}
|
||||
|
||||
export async function extractUrlAndDownloadImage(element, imageKey: string, dir: string) {
|
||||
const imageId = element.data[imageKey]
|
||||
let imagenUrl = imageId ? await downloadFile(imageId, dir) : null
|
||||
const returnValue = { ...element.data, [`${imageKey}_file`]: imagenUrl }
|
||||
|
||||
return returnValue
|
||||
}
|
||||
|
||||
const client = new DirectusSDK({
|
||||
mode: 'jwt',
|
||||
project: '_',
|
||||
url: process.env.DIRECTUS_HOST,
|
||||
})
|
||||
|
||||
export async function login() {
|
||||
return client.login({
|
||||
email: process.env.DIRECTUS_API_USER,
|
||||
password: process.env.DIRECTUS_API_PASSWORD,
|
||||
})
|
||||
}
|
||||
|
||||
export interface IWork {
|
||||
id: number
|
||||
titulo: string
|
||||
banner: number
|
||||
tags: string[]
|
||||
cliente: string
|
||||
pais: string
|
||||
year: string
|
||||
texto_descripcion: string
|
||||
color1: string
|
||||
color2: string
|
||||
imagenes: number[]
|
||||
slug: string
|
||||
}
|
||||
|
||||
export interface IWorkWithBanner extends IWork {
|
||||
banner_file: string | null
|
||||
}
|
||||
|
||||
export interface IWorkWithImages extends IWorkWithBanner {
|
||||
imagenes_files: string[] | null
|
||||
}
|
||||
|
||||
export async function getAllWorks() {
|
||||
return client.getItems<IWork[]>('work')
|
||||
}
|
||||
|
||||
export async function getWorkById(id: number) {
|
||||
return client.getItem<IWork>('work', id)
|
||||
}
|
||||
|
||||
export interface IAbout {
|
||||
texto_about: string
|
||||
texto_team: string
|
||||
}
|
||||
|
||||
export async function getAbout() {
|
||||
return client.getItem<IAbout>('about', 1)
|
||||
}
|
||||
|
||||
export interface IInicio {
|
||||
mensaje_inspirador: string
|
||||
fondo: number
|
||||
}
|
||||
|
||||
export interface IInicioWithImage {
|
||||
fondo_file: string
|
||||
}
|
||||
|
||||
export async function getInicio() {
|
||||
return client.getItem<IInicio[]>('inicio', 1)
|
||||
}
|
||||
|
||||
export interface ITeamMember {
|
||||
id: number
|
||||
nombre: string
|
||||
cita: string
|
||||
avatar: number
|
||||
}
|
||||
|
||||
export interface ITeamMemberWithImage {
|
||||
avatar_file: string
|
||||
}
|
||||
|
||||
export async function getAllTeam() {
|
||||
return client.getItems<ITeamMember[]>('el_team')
|
||||
}
|
||||
|
||||
export async function getTeamById(id: number) {
|
||||
return client.getItem<ITeamMember>('team', id)
|
||||
}
|
||||
|
||||
export interface INota {
|
||||
id: number
|
||||
titulo: string
|
||||
tiempo_lectura: number
|
||||
cuerpo: string
|
||||
tags: string[]
|
||||
slug: string
|
||||
imagen: number
|
||||
}
|
||||
|
||||
export interface INotaWithImage extends INota {
|
||||
imagen_file: string | null
|
||||
}
|
||||
|
||||
export async function getAllNotas() {
|
||||
return client.getItems<INota[]>('notas')
|
||||
}
|
||||
|
||||
export async function getNotaById(id: number) {
|
||||
return client.getItem<INota>('notas', id)
|
||||
}
|
||||
|
||||
export async function getAllNotasWithImages(): Promise<INotaWithImage[]> {
|
||||
const notas = await getAllNotas()
|
||||
|
||||
const notasWithImages: INotaWithImage[] = await Promise.all(
|
||||
notas.data.map(async (nota) =>
|
||||
extractUrlAndDownloadImage(nota, 'imagen', notasImagesDir),
|
||||
),
|
||||
)
|
||||
|
||||
return notasWithImages
|
||||
}
|
||||
|
||||
export async function getNotaWithImage(id: number): Promise<INotaWithImage> {
|
||||
const nota = await getNotaById(id)
|
||||
const notaWithImage = await extractUrlAndDownloadImage(nota, 'imagen', notasImagesDir)
|
||||
return notaWithImage
|
||||
}
|
||||
|
||||
export async function getAllWorksWithBanners(): Promise<IWorkWithBanner[]> {
|
||||
const works = await getAllWorks()
|
||||
|
||||
const worksWithBanners: IWorkWithBanner[] = await Promise.all(
|
||||
works.data.map(async (work) =>
|
||||
extractUrlAndDownloadImage(work, 'banner', workImagesDir),
|
||||
),
|
||||
)
|
||||
|
||||
return worksWithBanners
|
||||
}
|
||||
|
||||
export async function getWorkWithImages(id: number): Promise<IWorkWithImages> {
|
||||
const work = await getWorkById(id)
|
||||
const workWithBanner = extractUrlAndDownloadImage(work, 'banner', workImagesDir)
|
||||
const imageNames: string[] = await Promise.all(
|
||||
work.data.imagenes.map(async (image) => downloadFile(image, workImagesDir)),
|
||||
)
|
||||
|
||||
return { ...workWithBanner, imagenes_files: imageNames }
|
||||
}
|
||||
|
||||
export async function getTeamMembersWithImages(): Promise<ITeamMemberWithImage[]> {
|
||||
const team = await getAllTeam()
|
||||
|
||||
const teamWithAvatars: ITeamMemberWithImage[] = await Promise.all(
|
||||
team.data.map(async (member) =>
|
||||
extractUrlAndDownloadImage(member, 'avatar', avatarImagesDir),
|
||||
),
|
||||
)
|
||||
|
||||
return teamWithAvatars
|
||||
}
|
||||
|
||||
export async function getInicioWithImage(): Promise<IInicioWithImage> {
|
||||
const inicio = await getInicio()
|
||||
const inicioWithImage = await extractUrlAndDownloadImage(inicio, 'fondo', assetsDir)
|
||||
return inicioWithImage
|
||||
}
|
||||
|
||||
export default client
|
@ -0,0 +1,59 @@
|
||||
import { Flex, ChakraProvider } from '@chakra-ui/react'
|
||||
import { AppProps } from 'next/app'
|
||||
import { useRouter } from 'next/router'
|
||||
import NProgress from 'nprogress'
|
||||
import { useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
|
||||
import NavBar from '../components/NavBar'
|
||||
|
||||
import 'focus-visible/dist/focus-visible'
|
||||
import 'typeface-ibm-plex-sans'
|
||||
|
||||
import theme from '../theme'
|
||||
|
||||
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const setDocHeight = () => {
|
||||
document.documentElement.style.setProperty('--vh', `${window.innerHeight / 100}px`)
|
||||
}
|
||||
|
||||
const load = () => {
|
||||
NProgress.start()
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
NProgress.done()
|
||||
}
|
||||
|
||||
router.events.on('routeChangeStart', load)
|
||||
router.events.on('routeChangeComplete', stop)
|
||||
router.events.on('routeChangeError', stop)
|
||||
|
||||
setDocHeight()
|
||||
window.addEventListener('resize', function () {
|
||||
setDocHeight()
|
||||
})
|
||||
window.addEventListener('orientationchange', function () {
|
||||
setDocHeight()
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<ChakraProvider theme={theme} resetCSS>
|
||||
<Flex direction="column" minH="calc(var(--vh, 1vh) * 100)" w="100%">
|
||||
<NavBar />
|
||||
<Component {...pageProps} />
|
||||
</Flex>
|
||||
</ChakraProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
@ -0,0 +1,57 @@
|
||||
import getConfig from 'next/config'
|
||||
import Document, { Head, Html, Main, NextScript } from 'next/document'
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
const description =
|
||||
'Sitio web para Seminario de Gestion de Contenidos para la web llevado a cabo por Lautaro Valdez e Ian Mancini'
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="es">
|
||||
<Head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={`${publicRuntimeConfig.basePath}/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href={`${publicRuntimeConfig.basePath}/favicon-32x32.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href={`${publicRuntimeConfig.basePath}/favicon-16x16.png`}
|
||||
/>
|
||||
<link
|
||||
rel="manifest"
|
||||
href={`${publicRuntimeConfig.basePath}/site.webmanifest`}
|
||||
/>
|
||||
|
||||
<meta property="og:description" content={description} key="ogdesc" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={`${publicRuntimeConfig.basePath}/android-chrome-512x512.png`}
|
||||
key="ogimage"
|
||||
/>
|
||||
|
||||
<link rel="icon" href={`${publicRuntimeConfig.basePath}/favicon.ico`} />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
@ -0,0 +1,48 @@
|
||||
import { Text, Box } from '@chakra-ui/react'
|
||||
|
||||
import ResponsiveImage from '../components/ResponsiveImage'
|
||||
import SEO from '../components/SEO'
|
||||
|
||||
import { getInicioWithImage, login } from '../lib/api'
|
||||
|
||||
const Home: React.FC = ({ fondo_file, mensaje_inspirador }) => {
|
||||
return (
|
||||
<>
|
||||
<SEO />
|
||||
<Box direction="column" w="100%" position="relative" flex="1 0 0">
|
||||
<ResponsiveImage
|
||||
url={fondo_file}
|
||||
alt="Fondo de página de inicio"
|
||||
zIndex={0}
|
||||
w="100%"
|
||||
h="calc(var(--vh, 1vh) * 100 - var(--nav-height, 72px))"
|
||||
overflow="hidden"
|
||||
/>
|
||||
<Text
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
fontSize={['3xl', '3xl', '7xl']}
|
||||
fontWeight="700"
|
||||
lineHeight="1.2"
|
||||
pr="2rem"
|
||||
zIndex={1}
|
||||
position="absolute"
|
||||
textAlign="right"
|
||||
style={{ wordSpacing: '9999px' }}
|
||||
>
|
||||
{mensaje_inspirador}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
||||
export async function getStaticProps() {
|
||||
await login()
|
||||
|
||||
const data = await getInicioWithImage()
|
||||
|
||||
return { props: data }
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
type Path = {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const paths: Array<Path> = [
|
||||
{
|
||||
name: 'Inicio',
|
||||
path: '/',
|
||||
},
|
||||
{
|
||||
name: 'Work',
|
||||
path: '/work',
|
||||
},
|
||||
{
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
},
|
||||
{
|
||||
name: 'El team',
|
||||
path: '/team',
|
||||
},
|
||||
{
|
||||
name: 'Notas',
|
||||
path: '/notas',
|
||||
},
|
||||
{
|
||||
name: 'Contact',
|
||||
path: '/contact',
|
||||
},
|
||||
]
|
||||
|
||||
export default paths
|
@ -0,0 +1,78 @@
|
||||
import { extendTheme } from '@chakra-ui/react'
|
||||
|
||||
export const blue = '#3452FF'
|
||||
|
||||
const customTheme = extendTheme({
|
||||
colors: {
|
||||
blue,
|
||||
},
|
||||
fonts: {
|
||||
body: 'IBM Plex Sans, sans-serif',
|
||||
heading: 'IBM Plex Sans, sans-serif',
|
||||
},
|
||||
styles: {
|
||||
global: {
|
||||
'.headroom': {
|
||||
zIndex: '99 !important',
|
||||
},
|
||||
'#nprogress': {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'.hide-scrollbar, #__next': {
|
||||
'::webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
scrollbarWidth: 'none !important',
|
||||
},
|
||||
'#nprogress .bar': {
|
||||
background: 'blue',
|
||||
position: 'fixed',
|
||||
zIndex: 1031,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '3px',
|
||||
},
|
||||
a: {
|
||||
textDecoration: 'none !important',
|
||||
},
|
||||
'html, body': {
|
||||
fontFamily: 'IBM Plex Sans, sans-serif',
|
||||
bg: 'white',
|
||||
color: 'black',
|
||||
fontSize: 'xl',
|
||||
lineHeight: 'tall',
|
||||
width: '100%',
|
||||
minHeight: '100vh',
|
||||
},
|
||||
'.enable-scroll': {
|
||||
overflow: 'hidden auto',
|
||||
},
|
||||
'.lock-scroll': {
|
||||
overflow: 'hidden !important',
|
||||
},
|
||||
// '*': {
|
||||
// scrollbarWidth: 'auto',
|
||||
// scrollbarColor: `${green} black`,
|
||||
// },
|
||||
|
||||
// '*::-webkit-scrollbar': {
|
||||
// width: '12px',
|
||||
// height: '12px',
|
||||
// },
|
||||
// '*::-webkit-scrollbar-track': {
|
||||
// background: `#000000`,
|
||||
// },
|
||||
// '*::-webkit-scrollbar-thumb': {
|
||||
// backgroundColor: `${green}`,
|
||||
// borderRadius: '12px',
|
||||
// border: '2px solid black',
|
||||
// },
|
||||
// '*::-webkit-scrollbar-corner': {
|
||||
// backgroundColor: `#000000`,
|
||||
// },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default customTheme
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|