Bring components and api methods from Artimañas 2020 web

master
Ian Mancini 4 years ago
parent 42513a6b5c
commit 0a942c7db7

@ -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'],
},
],
},
}

42
.gitignore vendored

@ -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 @@
v14.3.0

@ -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)

File diff suppressed because one or more lines are too long

@ -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()

5
next-env.d.ts vendored

@ -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,
},
)

11972
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8C2 4.68629 4.68629 2 8 2H20C23.3137 2 26 4.68629 26 8V20C26 23.3137 23.3137 26 20 26H2V8Z" stroke="#3452FF" stroke-width="4"/>
<circle cx="11" cy="9" r="3" fill="#3452FF"/>
<circle cx="19" cy="9" r="3" fill="#3452FF"/>
</svg>

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"]
}
Loading…
Cancel
Save