├── server ├── .gitignore ├── prisma │ ├── dev.db │ ├── migrations │ │ ├── migration_lock.toml │ │ ├── 20230512190435_create_users_table │ │ │ └── migration.sql │ │ ├── 20230517190652_update_github_id_to_int │ │ │ └── migration.sql │ │ └── 20230516181747_create_memories_table │ │ │ └── migration.sql │ └── schema.prisma ├── src │ ├── lib │ │ └── prisma.ts │ ├── auth.d.ts │ ├── server.ts │ └── routes │ │ ├── upload.ts │ │ ├── auth.ts │ │ └── memories.ts ├── .eslintrc.json ├── package.json └── tsconfig.json ├── mobile ├── index.js ├── prettier.config.js ├── assets │ ├── icon.png │ ├── splash.png │ ├── favicon.png │ └── adaptive-icon.png ├── src │ ├── assets │ │ ├── bg-blur.png │ │ ├── assets.d.ts │ │ ├── nlw-spacetime-logo.svg │ │ └── stripes.svg │ └── lib │ │ └── api.ts ├── .eslintrc.json ├── tsconfig.json ├── babel.config.js ├── .gitignore ├── metro.config.js ├── app.json ├── tailwind.config.js ├── package.json └── app │ ├── _layout.tsx │ ├── index.tsx │ ├── memories.tsx │ └── new.tsx ├── web ├── prettier.config.js ├── src │ ├── app │ │ ├── icon.png │ │ ├── globals.css │ │ ├── api │ │ │ └── auth │ │ │ │ ├── logout │ │ │ │ └── route.ts │ │ │ │ └── callback │ │ │ │ └── route.ts │ │ ├── memories │ │ │ └── new │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── lib │ │ ├── api.ts │ │ └── auth.ts │ ├── components │ │ ├── Copyright.tsx │ │ ├── EmptyMemories.tsx │ │ ├── SignIn.tsx │ │ ├── Profile.tsx │ │ ├── Hero.tsx │ │ ├── MediaPicker.tsx │ │ └── NewMemoryForm.tsx │ ├── middleware.ts │ └── assets │ │ ├── nlw-spacetime-logo.svg │ │ └── bg-stars.svg ├── postcss.config.js ├── .eslintrc.json ├── next.config.js ├── .gitignore ├── tsconfig.json ├── package.json └── tailwind.config.js ├── LICENSE └── README.md /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /mobile/index.js: -------------------------------------------------------------------------------- 1 | import 'expo-router/entry' 2 | -------------------------------------------------------------------------------- /web/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('prettier-plugin-tailwindcss')], 3 | } 4 | -------------------------------------------------------------------------------- /mobile/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('prettier-plugin-tailwindcss')], 3 | } 4 | -------------------------------------------------------------------------------- /server/prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/nlw-12-spacetime-ignite/HEAD/server/prisma/dev.db -------------------------------------------------------------------------------- /web/src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/nlw-12-spacetime-ignite/HEAD/web/src/app/icon.png -------------------------------------------------------------------------------- /mobile/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/nlw-12-spacetime-ignite/HEAD/mobile/assets/icon.png -------------------------------------------------------------------------------- /mobile/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/nlw-12-spacetime-ignite/HEAD/mobile/assets/splash.png -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /mobile/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/nlw-12-spacetime-ignite/HEAD/mobile/assets/favicon.png -------------------------------------------------------------------------------- /web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "@rocketseat/eslint-config/react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /mobile/src/assets/bg-blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/nlw-12-spacetime-ignite/HEAD/mobile/src/assets/bg-blur.png -------------------------------------------------------------------------------- /web/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const api = axios.create({ 4 | baseURL: 'http://localhost:3333', 5 | }) 6 | -------------------------------------------------------------------------------- /mobile/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-education/nlw-12-spacetime-ignite/HEAD/mobile/assets/adaptive-icon.png -------------------------------------------------------------------------------- /mobile/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const api = axios.create({ 4 | baseURL: 'http://192.1.0.183:3333', 5 | }) 6 | -------------------------------------------------------------------------------- /server/src/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | export const prisma = new PrismaClient({ 4 | log: ['query'], 5 | }) 6 | -------------------------------------------------------------------------------- /mobile/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@rocketseat/eslint-config/react" 4 | ], 5 | "rules": { 6 | "camelcase": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@rocketseat/eslint-config/node" 4 | ], 5 | "rules": { 6 | "camelcase": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "nativewind/types" 5 | ] 6 | }, 7 | "extends": "expo/tsconfig.base" 8 | } 9 | -------------------------------------------------------------------------------- /server/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /server/prisma/migrations/20230512190435_create_users_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /web/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | -webkit-font-smoothing: antialiased; 7 | text-rendering: optimizeLegibility; 8 | } 9 | -------------------------------------------------------------------------------- /web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ['avatars.githubusercontent.com'], 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /mobile/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['nativewind/babel', require.resolve('expo-router/babel')], 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mobile/src/assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | 3 | declare module '*.svg' { 4 | import React from 'react' 5 | import { SvgProps } from 'react-native-svg' 6 | const content: React.FC 7 | export default content 8 | } 9 | -------------------------------------------------------------------------------- /server/src/auth.d.ts: -------------------------------------------------------------------------------- 1 | import '@fastify/jwt' 2 | 3 | declare module '@fastify/jwt' { 4 | export interface FastifyJWT { 5 | user: { 6 | sub: string 7 | name: string 8 | avatarUrl: string 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # Temporary files created by Metro to check the health of the file watcher 17 | .metro-health-check* 18 | -------------------------------------------------------------------------------- /web/src/app/api/auth/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | 3 | export async function GET(request: NextRequest) { 4 | const redirectURL = new URL('/', request.url) 5 | 6 | return NextResponse.redirect(redirectURL, { 7 | headers: { 8 | 'Set-Cookie': `token=; Path=/; max-age=0;`, 9 | }, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /web/src/components/Copyright.tsx: -------------------------------------------------------------------------------- 1 | export function Copyright() { 2 | return ( 3 |
4 | Feito com 💜 no NLW da{' '} 5 | 11 | Rocketseat 12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /web/src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers' 2 | import decode from 'jwt-decode' 3 | 4 | interface User { 5 | sub: string 6 | name: string 7 | avatarUrl: string 8 | } 9 | 10 | export function getUser(): User { 11 | const token = cookies().get('token')?.value 12 | 13 | if (!token) { 14 | throw new Error('Unauthenticated.') 15 | } 16 | 17 | const user: User = decode(token) 18 | 19 | return user 20 | } 21 | -------------------------------------------------------------------------------- /web/src/components/EmptyMemories.tsx: -------------------------------------------------------------------------------- 1 | export function EmptyMemories() { 2 | return ( 3 |
4 |

5 | Você ainda não registrou nenhuma lembrança, começa{' '} 6 | 7 | criar agora 8 | 9 | ! 10 |

11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /mobile/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config') 2 | 3 | module.exports = (() => { 4 | const config = getDefaultConfig(__dirname) 5 | 6 | const { transformer, resolver } = config 7 | 8 | config.transformer = { 9 | ...transformer, 10 | babelTransformerPath: require.resolve('react-native-svg-transformer'), 11 | } 12 | config.resolver = { 13 | ...resolver, 14 | assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'), 15 | sourceExts: [...resolver.sourceExts, 'svg'], 16 | } 17 | 18 | return config 19 | })() 20 | -------------------------------------------------------------------------------- /web/src/app/memories/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { NewMemoryForm } from '@/components/NewMemoryForm' 2 | import { ChevronLeft } from 'lucide-react' 3 | import Link from 'next/link' 4 | 5 | export default function NewMemory() { 6 | return ( 7 |
8 | 12 | 13 | voltar à timeline 14 | 15 | 16 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /web/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | 3 | const signInURL = `https://github.com/login/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID}` 4 | 5 | export function middleware(request: NextRequest) { 6 | const token = request.cookies.get('token')?.value 7 | 8 | if (!token) { 9 | return NextResponse.redirect(signInURL, { 10 | headers: { 11 | 'Set-Cookie': `redirectTo=${request.url}; Path=/; HttpOnly; max-age=20;`, 12 | }, 13 | }) 14 | } 15 | 16 | return NextResponse.next() 17 | } 18 | 19 | export const config = { 20 | matcher: '/memories/:path*', 21 | } 22 | -------------------------------------------------------------------------------- /server/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "sqlite" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | id String @id @default(uuid()) 12 | githubId Int @unique 13 | name String 14 | login String 15 | avatarUrl String 16 | 17 | memories Memory[] 18 | } 19 | 20 | model Memory { 21 | id String @id @default(uuid()) 22 | userId String 23 | 24 | coverUrl String 25 | content String 26 | isPublic Boolean @default(false) 27 | createdAt DateTime @default(now()) 28 | 29 | user User @relation(fields: [userId], references: [id]) 30 | } 31 | -------------------------------------------------------------------------------- /web/src/components/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { User } from 'lucide-react' 2 | 3 | export function SignIn() { 4 | return ( 5 | 9 |
10 | 11 |
12 | 13 |

14 | Crie sua conta e salve suas memórias 15 |

16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /web/src/components/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { getUser } from '@/lib/auth' 2 | import Image from 'next/image' 3 | 4 | export function Profile() { 5 | const { name, avatarUrl } = getUser() 6 | 7 | return ( 8 |
9 | 16 | 17 |

18 | {name} 19 | 23 | Quero sair 24 | 25 |

26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 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 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "mobile", 4 | "slug": "mobile", 5 | "scheme": "nlwspacetime", 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "userInterfaceStyle": "light", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": [ 16 | "**/*" 17 | ], 18 | "ios": { 19 | "supportsTablet": true 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | } 26 | }, 27 | "web": { 28 | "favicon": "./assets/favicon.png" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/src/app/api/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { api } from '@/lib/api' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | 4 | export async function GET(request: NextRequest) { 5 | const { searchParams } = new URL(request.url) 6 | const code = searchParams.get('code') 7 | 8 | const redirectTo = request.cookies.get('redirectTo')?.value 9 | 10 | const registerResponse = await api.post('/register', { 11 | code, 12 | }) 13 | 14 | const { token } = registerResponse.data 15 | 16 | const redirectURL = redirectTo ?? new URL('/', request.url) 17 | 18 | const cookieExpiresInSeconds = 60 * 60 * 24 * 30 19 | 20 | return NextResponse.redirect(redirectURL, { 21 | headers: { 22 | 'Set-Cookie': `token=${token}; Path=/; max-age=${cookieExpiresInSeconds};`, 23 | }, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "tsx watch src/server.ts", 8 | "lint": "eslint src --ext .ts --fix" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rocketseat/eslint-config": "^1.2.0", 15 | "@types/node": "^20.1.3", 16 | "dotenv": "^16.0.3", 17 | "eslint": "^8.40.0", 18 | "prisma": "^4.14.0", 19 | "tsx": "^3.12.7", 20 | "typescript": "^5.0.4" 21 | }, 22 | "dependencies": { 23 | "@fastify/cors": "^8.2.1", 24 | "@fastify/jwt": "^6.7.1", 25 | "@fastify/multipart": "^7.6.0", 26 | "@fastify/static": "^6.10.1", 27 | "@prisma/client": "^4.14.0", 28 | "axios": "^1.4.0", 29 | "fastify": "^4.17.0", 30 | "zod": "^3.21.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230517190652_update_github_id_to_int/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to alter the column `githubId` on the `User` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`. 5 | 6 | */ 7 | -- RedefineTables 8 | PRAGMA foreign_keys=OFF; 9 | CREATE TABLE "new_User" ( 10 | "id" TEXT NOT NULL PRIMARY KEY, 11 | "githubId" INTEGER NOT NULL, 12 | "name" TEXT NOT NULL, 13 | "login" TEXT NOT NULL, 14 | "avatarUrl" TEXT NOT NULL 15 | ); 16 | INSERT INTO "new_User" ("avatarUrl", "githubId", "id", "login", "name") SELECT "avatarUrl", "githubId", "id", "login", "name" FROM "User"; 17 | DROP TABLE "User"; 18 | ALTER TABLE "new_User" RENAME TO "User"; 19 | CREATE UNIQUE INDEX "User_githubId_key" ON "User"("githubId"); 20 | PRAGMA foreign_key_check; 21 | PRAGMA foreign_keys=ON; 22 | -------------------------------------------------------------------------------- /web/src/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | import nlwLogo from '../assets/nlw-spacetime-logo.svg' 4 | import Link from 'next/link' 5 | 6 | export function Hero() { 7 | return ( 8 |
9 | NLW Spacetime 10 | 11 |
12 |

13 | Sua cápsula do tempo 14 |

15 |

16 | Colecione momentos marcantes da sua jornada e compartilhe (se quiser) 17 | com o mundo! 18 |

19 |
20 | 21 | 25 | CADASTRAR LEMBRANÇA 26 | 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /web/src/components/MediaPicker.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ChangeEvent, useState } from 'react' 4 | 5 | export function MediaPicker() { 6 | const [preview, setPreview] = useState(null) 7 | 8 | function onFileSelected(event: ChangeEvent) { 9 | const { files } = event.target 10 | 11 | if (!files) { 12 | return 13 | } 14 | 15 | const previewURL = URL.createObjectURL(files[0]) 16 | 17 | setPreview(previewURL) 18 | } 19 | 20 | return ( 21 | <> 22 | 30 | 31 | {preview && ( 32 | // eslint-disable-next-line 33 | 38 | )} 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | import fastify from 'fastify' 4 | import cors from '@fastify/cors' 5 | import jwt from '@fastify/jwt' 6 | import multipart from '@fastify/multipart' 7 | import { memoriesRoutes } from './routes/memories' 8 | import { authRoutes } from './routes/auth' 9 | import { uploadRoutes } from './routes/upload' 10 | import { resolve } from 'node:path' 11 | 12 | const app = fastify() 13 | 14 | app.register(multipart) 15 | 16 | app.register(require('@fastify/static'), { 17 | root: resolve(__dirname, '../uploads'), 18 | prefix: '/uploads', 19 | }) 20 | 21 | app.register(cors, { 22 | origin: true, 23 | }) 24 | 25 | app.register(jwt, { 26 | secret: 'spacetime', 27 | }) 28 | 29 | app.register(authRoutes) 30 | app.register(uploadRoutes) 31 | app.register(memoriesRoutes) 32 | 33 | app 34 | .listen({ 35 | port: 3333, 36 | host: '0.0.0.0', 37 | }) 38 | .then(() => { 39 | console.log('🚀 HTTP server running on port http://localhost:3333') 40 | }) 41 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "20.1.3", 13 | "@types/react": "18.2.6", 14 | "@types/react-dom": "18.2.4", 15 | "autoprefixer": "10.4.14", 16 | "axios": "^1.4.0", 17 | "dayjs": "^1.11.7", 18 | "eslint": "8.40.0", 19 | "eslint-config-next": "13.4.2", 20 | "js-cookie": "^3.0.5", 21 | "jwt-decode": "^3.1.2", 22 | "lucide-react": "^0.216.0", 23 | "next": "13.4.2", 24 | "postcss": "8.4.23", 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0", 27 | "tailwindcss": "3.3.2", 28 | "typescript": "5.0.4" 29 | }, 30 | "devDependencies": { 31 | "@rocketseat/eslint-config": "^1.2.0", 32 | "@tailwindcss/forms": "^0.5.3", 33 | "@types/js-cookie": "^3.0.3", 34 | "prettier-plugin-tailwindcss": "^0.2.8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rocketseat Education 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Rocketseat Education 3 |

4 | 5 |

6 | Rocketseat Project 7 | License 8 |

9 | 10 | ## 💻 Projeto 11 | 12 | Aplicação de recordação de memórias, onde o usuário poderá adicionar à uma timeline textos, fotos e vídeos de acontecimentos marcantes da sua vida, organizados por mês e ano. 13 | 14 | ## 📝 Licença 15 | 16 | Esse projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes. 17 | 18 | --- 19 | 20 |

21 | Feito com 💜 by Rocketseat 22 |

23 | 24 | 25 | 26 |
27 |
28 | 29 |

30 | 31 | banner 32 | 33 |

34 | 35 | 36 | -------------------------------------------------------------------------------- /mobile/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./App.tsx', './app/**/*.tsx'], 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | title: 'Roboto_700Bold', 8 | body: 'Roboto_400Regular', 9 | alt: 'BaiJamjuree_700Bold', 10 | }, 11 | 12 | colors: { 13 | gray: { 14 | 50: '#eaeaea', 15 | 100: '#bebebf', 16 | 200: '#9e9ea0', 17 | 300: '#727275', 18 | 400: '#56565a', 19 | 500: '#2c2c31', 20 | 600: '#28282d', 21 | 700: '#1f1f23', 22 | 800: '#18181b', 23 | 900: '#121215', 24 | }, 25 | purple: { 26 | 50: '#f3eefc', 27 | 100: '#d8cbf7', 28 | 200: '#c6b2f3', 29 | 300: '#ab8eee', 30 | 400: '#9b79ea', 31 | 500: '#8257e5', 32 | 600: '#764fd0', 33 | 700: '#5c3ea3', 34 | 800: '#48307e', 35 | 900: '#372560', 36 | }, 37 | green: { 38 | 50: '#e6fbef', 39 | 100: '#b1f1ce', 40 | 200: '#8cebb6', 41 | 300: '#57e295', 42 | 400: '#36dc81', 43 | 500: '#04d361', 44 | 600: '#04c058', 45 | 700: '#039645', 46 | 800: '#027435', 47 | 900: '#025929', 48 | }, 49 | }, 50 | }, 51 | }, 52 | plugins: [], 53 | } 54 | -------------------------------------------------------------------------------- /server/src/routes/upload.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | import { extname, resolve } from 'node:path' 3 | import { FastifyInstance } from 'fastify' 4 | import { createWriteStream } from 'node:fs' 5 | import { pipeline } from 'node:stream' 6 | import { promisify } from 'node:util' 7 | 8 | const pump = promisify(pipeline) 9 | 10 | export async function uploadRoutes(app: FastifyInstance) { 11 | app.post('/upload', async (request, reply) => { 12 | const upload = await request.file({ 13 | limits: { 14 | fileSize: 5_242_880, // 5mb 15 | }, 16 | }) 17 | 18 | if (!upload) { 19 | return reply.status(400).send() 20 | } 21 | 22 | const mimeTypeRegex = /^(image|video)\/[a-zA-Z]+/ 23 | const isValidFileFormat = mimeTypeRegex.test(upload.mimetype) 24 | 25 | if (!isValidFileFormat) { 26 | return reply.status(400).send() 27 | } 28 | 29 | const fileId = randomUUID() 30 | const extension = extname(upload.filename) 31 | 32 | const fileName = fileId.concat(extension) 33 | 34 | const writeStream = createWriteStream( 35 | resolve(__dirname, '..', '..', 'uploads', fileName), 36 | ) 37 | 38 | await pump(upload.file, writeStream) 39 | 40 | const fullUrl = request.protocol.concat('://').concat(request.hostname) 41 | const fileUrl = new URL(`/uploads/${fileName}`, fullUrl).toString() 42 | 43 | return { fileUrl } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230516181747_create_memories_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `avatarUrl` to the `User` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `githubId` to the `User` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `login` to the `User` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- CreateTable 10 | CREATE TABLE "Memory" ( 11 | "id" TEXT NOT NULL PRIMARY KEY, 12 | "userId" TEXT NOT NULL, 13 | "coverUrl" TEXT NOT NULL, 14 | "content" TEXT NOT NULL, 15 | "isPublic" BOOLEAN NOT NULL DEFAULT false, 16 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | CONSTRAINT "Memory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 18 | ); 19 | 20 | -- RedefineTables 21 | PRAGMA foreign_keys=OFF; 22 | CREATE TABLE "new_User" ( 23 | "id" TEXT NOT NULL PRIMARY KEY, 24 | "githubId" TEXT NOT NULL, 25 | "name" TEXT NOT NULL, 26 | "login" TEXT NOT NULL, 27 | "avatarUrl" TEXT NOT NULL 28 | ); 29 | INSERT INTO "new_User" ("id", "name") SELECT "id", "name" FROM "User"; 30 | DROP TABLE "User"; 31 | ALTER TABLE "new_User" RENAME TO "User"; 32 | CREATE UNIQUE INDEX "User_githubId_key" ON "User"("githubId"); 33 | PRAGMA foreign_key_check; 34 | PRAGMA foreign_keys=ON; 35 | -------------------------------------------------------------------------------- /mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@expo-google-fonts/bai-jamjuree": "^0.2.3", 13 | "@expo-google-fonts/roboto": "^0.2.3", 14 | "@types/react": "~18.0.27", 15 | "axios": "^1.4.0", 16 | "dayjs": "^1.11.7", 17 | "expo": "~48.0.15", 18 | "expo-auth-session": "~4.0.3", 19 | "expo-constants": "~14.2.1", 20 | "expo-crypto": "~12.2.1", 21 | "expo-font": "~11.1.1", 22 | "expo-image-picker": "~14.1.1", 23 | "expo-linking": "~4.0.1", 24 | "expo-router": "^1.5.3", 25 | "expo-secure-store": "~12.1.1", 26 | "expo-status-bar": "~1.4.4", 27 | "nativewind": "^2.0.11", 28 | "react": "18.2.0", 29 | "react-native": "0.71.7", 30 | "react-native-safe-area-context": "4.5.0", 31 | "react-native-screens": "~3.20.0", 32 | "react-native-svg": "13.4.0", 33 | "typescript": "^4.9.4" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.20.0", 37 | "@rocketseat/eslint-config": "^1.2.0", 38 | "eslint": "^8.40.0", 39 | "prettier-plugin-tailwindcss": "^0.2.8", 40 | "react-native-svg-transformer": "^1.0.0", 41 | "tailwindcss": "^3.3.2" 42 | }, 43 | "overrides": { 44 | "metro": "0.76.0", 45 | "metro-resolver": "0.76.0" 46 | }, 47 | "private": true 48 | } 49 | -------------------------------------------------------------------------------- /mobile/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'nativewind' 2 | import { ImageBackground } from 'react-native' 3 | 4 | import { 5 | useFonts, 6 | Roboto_400Regular, 7 | Roboto_700Bold, 8 | } from '@expo-google-fonts/roboto' 9 | 10 | import { BaiJamjuree_700Bold } from '@expo-google-fonts/bai-jamjuree' 11 | 12 | import blurBg from '../src/assets/bg-blur.png' 13 | import Stripes from '../src/assets/stripes.svg' 14 | import { SplashScreen, Stack } from 'expo-router' 15 | import { StatusBar } from 'expo-status-bar' 16 | import * as SecureStore from 'expo-secure-store' 17 | import { useEffect, useState } from 'react' 18 | 19 | const StyledStripes = styled(Stripes) 20 | 21 | export default function Layout() { 22 | const [isUserAuthenticated, setIsUserAuthenticated] = useState< 23 | null | boolean 24 | >(null) 25 | 26 | const [hasLoadedFonts] = useFonts({ 27 | Roboto_400Regular, 28 | Roboto_700Bold, 29 | BaiJamjuree_700Bold, 30 | }) 31 | 32 | useEffect(() => { 33 | SecureStore.getItemAsync('token').then((token) => { 34 | setIsUserAuthenticated(!!token) 35 | }) 36 | }, []) 37 | 38 | if (!hasLoadedFonts) { 39 | return 40 | } 41 | 42 | return ( 43 | 48 | 49 | 50 | 51 | 58 | 59 | 60 | 61 | 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | sans: 'var(--font-roboto)', 12 | alt: 'var(--font-bai-jamjuree)', 13 | }, 14 | 15 | colors: { 16 | gray: { 17 | 50: '#eaeaea', 18 | 100: '#bebebf', 19 | 200: '#9e9ea0', 20 | 300: '#727275', 21 | 400: '#56565a', 22 | 500: '#2c2c31', 23 | 600: '#28282d', 24 | 700: '#1f1f23', 25 | 800: '#18181b', 26 | 900: '#121215', 27 | }, 28 | purple: { 29 | 50: '#f3eefc', 30 | 100: '#d8cbf7', 31 | 200: '#c6b2f3', 32 | 300: '#ab8eee', 33 | 400: '#9b79ea', 34 | 500: '#8257e5', 35 | 600: '#764fd0', 36 | 700: '#5c3ea3', 37 | 800: '#48307e', 38 | 900: '#372560', 39 | }, 40 | green: { 41 | 50: '#e6fbef', 42 | 100: '#b1f1ce', 43 | 200: '#8cebb6', 44 | 300: '#57e295', 45 | 400: '#36dc81', 46 | 500: '#04d361', 47 | 600: '#04c058', 48 | 700: '#039645', 49 | 800: '#027435', 50 | 900: '#025929', 51 | }, 52 | }, 53 | 54 | backgroundImage: { 55 | stripes: 56 | 'linear-gradient(to bottom, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.1) 12.5%, transparent 12.5%, transparent)', 57 | }, 58 | 59 | fontSize: { 60 | '5xl': '2.5rem', 61 | }, 62 | 63 | backgroundSize: { 64 | stripes: '100% 8px', 65 | }, 66 | 67 | blur: { 68 | full: '194px', 69 | }, 70 | }, 71 | }, 72 | plugins: [require('@tailwindcss/forms')], 73 | } 74 | -------------------------------------------------------------------------------- /server/src/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify' 2 | import axios from 'axios' 3 | import { z } from 'zod' 4 | import { prisma } from '../lib/prisma' 5 | 6 | export async function authRoutes(app: FastifyInstance) { 7 | app.post('/register', async (request) => { 8 | const bodySchema = z.object({ 9 | code: z.string(), 10 | }) 11 | 12 | const { code } = bodySchema.parse(request.body) 13 | 14 | const accessTokenResponse = await axios.post( 15 | 'https://github.com/login/oauth/access_token', 16 | null, 17 | { 18 | params: { 19 | client_id: process.env.GITHUB_CLIENT_ID, 20 | client_secret: process.env.GITHUB_CLIENT_SECRET, 21 | code, 22 | }, 23 | headers: { 24 | Accept: 'application/json', 25 | }, 26 | }, 27 | ) 28 | 29 | const { access_token } = accessTokenResponse.data 30 | 31 | const userResponse = await axios.get('https://api.github.com/user', { 32 | headers: { 33 | Authorization: `Bearer ${access_token}`, 34 | }, 35 | }) 36 | 37 | const userSchema = z.object({ 38 | id: z.number(), 39 | login: z.string(), 40 | name: z.string(), 41 | avatar_url: z.string().url(), 42 | }) 43 | 44 | const userInfo = userSchema.parse(userResponse.data) 45 | 46 | let user = await prisma.user.findUnique({ 47 | where: { 48 | githubId: userInfo.id, 49 | }, 50 | }) 51 | 52 | if (!user) { 53 | user = await prisma.user.create({ 54 | data: { 55 | githubId: userInfo.id, 56 | login: userInfo.login, 57 | name: userInfo.name, 58 | avatarUrl: userInfo.avatar_url, 59 | }, 60 | }) 61 | } 62 | 63 | const token = app.jwt.sign( 64 | { 65 | name: user.name, 66 | avatarUrl: user.avatarUrl, 67 | }, 68 | { 69 | sub: user.id, 70 | expiresIn: '30 days', 71 | }, 72 | ) 73 | 74 | return { 75 | token, 76 | } 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /web/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { ReactNode } from 'react' 3 | import { 4 | Roboto_Flex as Roboto, 5 | Bai_Jamjuree as BaiJamjuree, 6 | } from 'next/font/google' 7 | 8 | import { Hero } from '@/components/Hero' 9 | import { Profile } from '@/components/Profile' 10 | import { SignIn } from '@/components/SignIn' 11 | import { Copyright } from '@/components/Copyright' 12 | import { cookies } from 'next/headers' 13 | 14 | const roboto = Roboto({ subsets: ['latin'], variable: '--font-roboto' }) 15 | 16 | const baiJamjuree = BaiJamjuree({ 17 | subsets: ['latin'], 18 | weight: '700', 19 | variable: '--font-bai-jamjuree', 20 | }) 21 | 22 | export const metadata = { 23 | title: 'NLW Spacetime', 24 | description: 25 | 'Uma cápsula do tempo construída com React, Next.js, TailwindCSS e Typescript.', 26 | } 27 | 28 | export default function RootLayout({ children }: { children: ReactNode }) { 29 | const isAuthenticated = cookies().has('token') 30 | 31 | return ( 32 | 33 | 36 |
37 | {/* Left */} 38 |
39 | {/* Blur */} 40 |
41 | 42 | {/* Stripes */} 43 |
44 | 45 | {isAuthenticated ? : } 46 | 47 | 48 |
49 | 50 | {/* Right */} 51 |
52 | {children} 53 |
54 |
55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /web/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { EmptyMemories } from '@/components/EmptyMemories' 2 | import { api } from '@/lib/api' 3 | import { cookies } from 'next/headers' 4 | import dayjs from 'dayjs' 5 | import ptBR from 'dayjs/locale/pt-br' 6 | import Image from 'next/image' 7 | import Link from 'next/link' 8 | import { ArrowRight } from 'lucide-react' 9 | 10 | dayjs.locale(ptBR) 11 | 12 | interface Memory { 13 | id: string 14 | coverUrl: string 15 | excerpt: string 16 | createdAt: string 17 | } 18 | 19 | export default async function Home() { 20 | const isAuthenticated = cookies().has('token') 21 | 22 | if (!isAuthenticated) { 23 | return 24 | } 25 | 26 | const token = cookies().get('token')?.value 27 | 28 | const response = await api.get('/memories', { 29 | headers: { 30 | Authorization: `Bearer ${token}`, 31 | }, 32 | }) 33 | 34 | const memories: Memory[] = response.data 35 | 36 | if (memories.length === 0) { 37 | return 38 | } 39 | 40 | return ( 41 |
42 | {memories.map((memory) => { 43 | return ( 44 |
45 | 48 | 55 |

56 | {memory.excerpt} 57 |

58 | 62 | Ler mais 63 | 64 | 65 |
66 | ) 67 | })} 68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /mobile/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { useRouter } from 'expo-router' 3 | import { Text, TouchableOpacity, View } from 'react-native' 4 | import { makeRedirectUri, useAuthRequest } from 'expo-auth-session' 5 | import * as SecureStore from 'expo-secure-store' 6 | 7 | import NLWLogo from '../src/assets/nlw-spacetime-logo.svg' 8 | import { api } from '../src/lib/api' 9 | 10 | const discovery = { 11 | authorizationEndpoint: 'https://github.com/login/oauth/authorize', 12 | tokenEndpoint: 'https://github.com/login/oauth/access_token', 13 | revocationEndpoint: 14 | 'https://github.com/settings/connections/applications/d26f194cc5d5132a51be', 15 | } 16 | 17 | export default function App() { 18 | const router = useRouter() 19 | 20 | const [, response, signInWithGithub] = useAuthRequest( 21 | { 22 | clientId: 'd26f194cc5d5132a51be', 23 | scopes: ['identity'], 24 | redirectUri: makeRedirectUri({ 25 | scheme: 'nlwspacetime', 26 | }), 27 | }, 28 | discovery, 29 | ) 30 | 31 | async function handleGithubOAuthCode(code: string) { 32 | const response = await api.post('/register', { 33 | code, 34 | }) 35 | 36 | const { token } = response.data 37 | 38 | await SecureStore.setItemAsync('token', token) 39 | 40 | router.push('/memories') 41 | } 42 | 43 | useEffect(() => { 44 | // console.log( 45 | // 'response', 46 | // makeRedirectUri({ 47 | // scheme: 'nlwspacetime', 48 | // }), 49 | // ) 50 | 51 | if (response?.type === 'success') { 52 | const { code } = response.params 53 | 54 | handleGithubOAuthCode(code) 55 | } 56 | }, [response]) 57 | 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | Sua cápsula do tempo 66 | 67 | 68 | Colecione momentos marcantes da sua jornada e compartilhe (se 69 | quiser) com o mundo! 70 | 71 | 72 | 73 | signInWithGithub()} 77 | > 78 | 79 | Cadastrar lembrança 80 | 81 | 82 | 83 | 84 | 85 | Feito com 💜 no NLW da Rocketseat 86 | 87 | 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /web/src/components/NewMemoryForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Camera } from 'lucide-react' 4 | import { MediaPicker } from './MediaPicker' 5 | import { FormEvent } from 'react' 6 | import { api } from '@/lib/api' 7 | import Cookie from 'js-cookie' 8 | import { useRouter } from 'next/navigation' 9 | 10 | export function NewMemoryForm() { 11 | const router = useRouter() 12 | 13 | async function handleCreateMemory(event: FormEvent) { 14 | event.preventDefault() 15 | 16 | const formData = new FormData(event.currentTarget) 17 | 18 | const fileToUpload = formData.get('coverUrl') 19 | 20 | let coverUrl = '' 21 | 22 | if (fileToUpload) { 23 | const uploadFormData = new FormData() 24 | uploadFormData.set('file', fileToUpload) 25 | 26 | const uploadResponse = await api.post('/upload', uploadFormData) 27 | 28 | coverUrl = uploadResponse.data.fileUrl 29 | } 30 | 31 | const token = Cookie.get('token') 32 | 33 | await api.post( 34 | '/memories', 35 | { 36 | coverUrl, 37 | content: formData.get('content'), 38 | isPublic: formData.get('isPublic'), 39 | }, 40 | { 41 | headers: { 42 | Authorization: `Bearer ${token}`, 43 | }, 44 | }, 45 | ) 46 | 47 | router.push('/') 48 | } 49 | 50 | return ( 51 |
52 |
53 | 60 | 61 | 74 |
75 | 76 | 77 | 78 |