initial commit
3
.env
Normal file
@ -0,0 +1,3 @@
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
POSTGRES_DB=info
|
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
frontend/node_modules
|
||||
go.sum
|
||||
social-network
|
||||
users.db
|
||||
fiber
|
||||
frontend/.next/
|
||||
/out/
|
7
Dockerfile
Normal file
@ -0,0 +1,7 @@
|
||||
FROM nginx:latest
|
||||
|
||||
WORKDIR /etc/nginx/
|
||||
|
||||
COPY nginx.conf .
|
||||
|
||||
EXPOSE 80
|
50
README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# CopyBook
|
||||
|
||||
A messageboard type social website
|
||||
|
||||
## Backend
|
||||
Fully coded in golang utilizing the following technologies:
|
||||
- https://github.com/gofiber/fiber
|
||||
- https://gorm.io/
|
||||
|
||||
The backend consists of multiple modules:
|
||||
- db.go: database modules setup with gorm
|
||||
- auth.go: authorization module with jwt tokens setup
|
||||
- server.go: main buisness logic functionality
|
||||
|
||||
|
||||
|
||||
## Frontend
|
||||
Typescript + nextjs + tailwindcss
|
||||
|
||||
the frontend uses nextjs for server side rendering.
|
||||
|
||||
there are multiple pages each responsible for its own functionality:
|
||||
- index.tsx: mounted on `/` is the homepage for the messageboard.
|
||||
- login.tsx: mounted on `/login` the login page
|
||||
- logout.tsx: mounted on `/logout` for logging out and removing the jwt token.
|
||||
- profile.tsx: mounted on `/profile` is for the user profile info.
|
||||
- signup.tsx: mounted on `/signup` is for registering new users.
|
||||
|
||||
|
||||
|
||||
|
||||
## Devops
|
||||
Fully dockerized and ready for deployment on any environment
|
||||
|
||||
backend setup consists of 2 containers one that builds the golang binary for and another that runs the server
|
||||
|
||||
the database for the backend is running a postgresql server with persistent storage enabled
|
||||
|
||||
frontend setup has 3 containers:
|
||||
`deps`: pulls the dependecies required for compiling the typescript files.
|
||||
`builder`: compiles the typescript files.
|
||||
`runner`: runs the nextjs server.
|
||||
|
||||
all of the previous services are running on `internalnet` virtual network and then afterwards its served with a nginx reverse proxy for traffic management and redundancy purposes
|
||||
|
||||
## TODO
|
||||
- better db management
|
||||
- more documentation
|
||||
- user personalization
|
||||
- more ui changes
|
13
backend/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM golang AS build
|
||||
# RUN apk --no-cache add gcc g++ make git
|
||||
WORKDIR /go/src/app
|
||||
COPY . .
|
||||
RUN go mod tidy
|
||||
# RUN GOOS=linux go build -ldflags="-s -w" -o ./bin/web-app
|
||||
RUN GOOS=linux go build -o ./bin/web-app
|
||||
|
||||
FROM ubuntu
|
||||
WORKDIR /usr/bin
|
||||
COPY --from=build /go/src/app/bin /go/bin
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT /go/bin/web-app
|
3
backend/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Backend API for the social network
|
||||
|
||||
rest api utlizing golang and the gofiber framework
|
35
backend/auth.go
Normal file
@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var jwtKey = []byte("ShXYLRYfFOw+upPD")
|
||||
|
||||
type Claim struct {
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func Sign(username string) (string, error) {
|
||||
expirationTime := jwt.NewNumericDate(time.Now().Add(5 * time.Hour))
|
||||
|
||||
claims := &Claim{
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: expirationTime,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
tokenString, err := token.SignedString(jwtKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, err
|
||||
|
||||
}
|
75
backend/db.go
Normal file
@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string
|
||||
Password string
|
||||
Email string
|
||||
Active bool
|
||||
}
|
||||
|
||||
type APIUser struct {
|
||||
ID uint
|
||||
Username string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
UID uint `gorm:"primaryKey"`
|
||||
PostId uint
|
||||
Commenter string
|
||||
Text string
|
||||
}
|
||||
|
||||
type APIComment struct {
|
||||
PostId uint `json:"post_id"`
|
||||
Text string `json:"content"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Content string
|
||||
Username string
|
||||
Comments string
|
||||
}
|
||||
|
||||
func initDB() *gorm.DB {
|
||||
dburl := "host=db user=root password=root dbname=info port=5432 sslmode=disable TimeZone=Asia/Shanghai"
|
||||
db, err := gorm.Open(postgres.Open(dburl), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
panic("failed to open db")
|
||||
}
|
||||
|
||||
db.AutoMigrate(&User{})
|
||||
db.AutoMigrate(&Post{})
|
||||
return db
|
||||
}
|
||||
|
||||
func validateUser(u User) bool {
|
||||
if len(u.Username) < 3 {
|
||||
return false
|
||||
}
|
||||
if len(u.Password) < 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
26
backend/go.mod
Normal file
@ -0,0 +1,26 @@
|
||||
module social-network
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.11.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/gofiber/cli v0.0.9 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.34.0
|
||||
github.com/gofiber/jwt/v3 v3.2.12 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.13 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.12.0 // indirect
|
||||
github.com/spf13/cobra v1.5.0 // indirect
|
||||
golang.org/x/crypto v0.22.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
gorm.io/driver/postgres v1.5.7 // indirect
|
||||
gorm.io/driver/sqlite v1.3.4 // indirect
|
||||
gorm.io/gorm v1.25.9 // indirect
|
||||
)
|
232
backend/server.go
Normal file
@ -0,0 +1,232 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
jwtware "github.com/gofiber/jwt/v3"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type Creds struct {
|
||||
Username string `json:"username" xml:"username" form:"username"`
|
||||
Pass string `json:"pass" xml:"pass" form:"pass"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type JsonComment struct {
|
||||
Array []Comment
|
||||
}
|
||||
|
||||
// go binary encoder
|
||||
func ToGOB64(m JsonComment) string {
|
||||
b := bytes.Buffer{}
|
||||
e := gob.NewEncoder(&b)
|
||||
err := e.Encode(m)
|
||||
if err != nil {
|
||||
fmt.Println(`failed gob Encode`, err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b.Bytes())
|
||||
}
|
||||
|
||||
// go binary decoder
|
||||
func FromGOB64(str string) JsonComment {
|
||||
m := JsonComment{}
|
||||
by, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
fmt.Println(`failed base64 Decode`, err)
|
||||
}
|
||||
b := bytes.Buffer{}
|
||||
b.Write(by)
|
||||
d := gob.NewDecoder(&b)
|
||||
err = d.Decode(&m)
|
||||
if err != nil {
|
||||
fmt.Println(`failed gob Decode`, err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func malformedToken(c *fiber.Ctx, w error) error {
|
||||
res := Response{Status: "bad", Text: "Malformed token"}
|
||||
return c.JSON(res)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := fiber.New()
|
||||
db := initDB()
|
||||
bad := Response{Status: "bad", Text: "Unauthorized"}
|
||||
errResponse := Response{Status: "bad", Text: "Woops something went wrong ¯\\_(ツ)_/¯"}
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "*",
|
||||
AllowHeaders: "Origin, Content-Type, Accept, Authorization",
|
||||
}))
|
||||
app.Post("/login", func(c *fiber.Ctx) error {
|
||||
p := new(Creds)
|
||||
if err := c.BodyParser(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var res User
|
||||
if err := db.First(&res, "Username = ?", p.Username).Error; err != nil {
|
||||
return c.JSON(bad)
|
||||
}
|
||||
|
||||
if !CheckPasswordHash(p.Pass, res.Password) {
|
||||
return c.JSON(bad)
|
||||
}
|
||||
|
||||
token, _ := Sign(p.Username)
|
||||
response := Response{Status: "ok", Text: token}
|
||||
return c.JSON(response)
|
||||
})
|
||||
|
||||
app.Post("/signup", func(c *fiber.Ctx) error {
|
||||
details := new(User)
|
||||
|
||||
if err := c.BodyParser(details); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !validateUser(*details) {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
details.Active = true
|
||||
|
||||
var query User
|
||||
if err := db.First(&query, "Username = ?", details.Username).Error; err != nil && query.Username != "" {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
|
||||
hashedPass, err := HashPassword(details.Password)
|
||||
if err != nil {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
|
||||
details.Password = hashedPass
|
||||
|
||||
db.Create(details)
|
||||
newUser := Response{Status: "ok", Text: "user created successfully"}
|
||||
return c.JSON(newUser)
|
||||
})
|
||||
|
||||
app.Use(jwtware.New(jwtware.Config{
|
||||
SigningKey: []byte("ShXYLRYfFOw+upPD"),
|
||||
ErrorHandler: malformedToken,
|
||||
}))
|
||||
|
||||
app.Get("/posts", func(c *fiber.Ctx) error {
|
||||
var query []Post
|
||||
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8)
|
||||
|
||||
db.Find(&query)
|
||||
|
||||
res, err := json.Marshal(query)
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
|
||||
return c.SendString(string(res))
|
||||
})
|
||||
|
||||
app.Get("/posts/:uid", func(c *fiber.Ctx) error {
|
||||
var post Post
|
||||
|
||||
db.Select("content").Find(&post, "ID = ?", c.Params("uid"))
|
||||
res := Response{Status: "ok", Text: post.Content}
|
||||
return c.JSON(res)
|
||||
})
|
||||
|
||||
app.Get("/profile/:uid", func(c *fiber.Ctx) error {
|
||||
var user APIUser
|
||||
if err := db.Model(&User{}).Find(&user, "Username = ?", c.Params("uid")).Error; err != nil {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
return c.JSON(user)
|
||||
})
|
||||
|
||||
app.Get("/profile", func(c *fiber.Ctx) error {
|
||||
var user APIUser
|
||||
token := c.Locals("user").(*jwt.Token)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
name := claims["username"].(string)
|
||||
|
||||
if err := db.Model(&User{}).Find(&user, "Username = ?", name).Error; err != nil {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
return c.JSON(user)
|
||||
})
|
||||
|
||||
app.Post("/create", func(c *fiber.Ctx) error {
|
||||
info := new(Post)
|
||||
|
||||
if err := c.BodyParser(info); err != nil {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
|
||||
user := c.Locals("user").(*jwt.Token)
|
||||
claims := user.Claims.(jwt.MapClaims)
|
||||
name := claims["username"].(string)
|
||||
|
||||
//figure out how to store user id's instead of usernames
|
||||
/*
|
||||
var query User
|
||||
if err := db.First(&query, "Username = ?", name).Error; err != nil && query.Username != "" {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
info.Uuid = query.ID
|
||||
*/
|
||||
info.Username = name
|
||||
db.Create(info)
|
||||
newPost := Response{Status: "ok", Text: "post created successfully"}
|
||||
return c.JSON(newPost)
|
||||
})
|
||||
|
||||
app.Post("/comment", func(c *fiber.Ctx) error {
|
||||
var comment Comment
|
||||
apiComment := new(APIComment)
|
||||
if err := c.BodyParser(apiComment); err != nil {
|
||||
return c.JSON(errResponse)
|
||||
}
|
||||
|
||||
user := c.Locals("user").(*jwt.Token)
|
||||
claims := user.Claims.(jwt.MapClaims)
|
||||
name := claims["username"].(string)
|
||||
|
||||
comment.PostId = apiComment.PostId
|
||||
comment.Text = apiComment.Text
|
||||
comment.Commenter = name
|
||||
|
||||
var post Post
|
||||
|
||||
db.Select("comments").Find(&post, "ID = ?", comment.PostId)
|
||||
dbComments := FromGOB64(post.Comments)
|
||||
|
||||
comments := JsonComment{Array: append([]Comment{comment}, dbComments.Array...)}
|
||||
store := ToGOB64(comments)
|
||||
db.Model(&Post{}).Where("ID = ?", comment.PostId).Update("comments", store)
|
||||
newComment := Response{Status: "ok", Text: "Comment created successfully"}
|
||||
return c.JSON(newComment)
|
||||
})
|
||||
|
||||
app.Get("/comments/:uid", func(c *fiber.Ctx) error {
|
||||
var post Post
|
||||
|
||||
db.Select("comments").Find(&post, "ID = ?", c.Params("uid"))
|
||||
comments := FromGOB64(post.Comments)
|
||||
return c.JSON(comments)
|
||||
})
|
||||
|
||||
log.Fatal(app.Listen(":3000"))
|
||||
}
|
58
docker-compose.yml
Normal file
@ -0,0 +1,58 @@
|
||||
services:
|
||||
|
||||
nginx:
|
||||
build: .
|
||||
container_name: nginx
|
||||
restart: "always"
|
||||
privileged: true
|
||||
links:
|
||||
- "go-api:api"
|
||||
- "frontend:frontend"
|
||||
ports:
|
||||
- 80:80
|
||||
networks:
|
||||
- internalnet
|
||||
|
||||
go-api:
|
||||
build: ./backend
|
||||
restart: "always"
|
||||
container_name: api
|
||||
environment:
|
||||
- DB_USER=${POSTGRES_USER}
|
||||
- DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- DB_NAME=${POSTGRES_DB}
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
ports:
|
||||
- 3000:3000
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- internalnet
|
||||
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
restart: "always"
|
||||
container_name: frontend
|
||||
ports:
|
||||
- 3001:3000
|
||||
networks:
|
||||
- internalnet
|
||||
db:
|
||||
image: postgres:latest
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- internalnet
|
||||
|
||||
networks:
|
||||
internalnet:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
7
frontend/.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
1
frontend/.env
Normal file
@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_URL=127.0.0.1:3000
|
60
frontend/Dockerfile
Normal file
@ -0,0 +1,60 @@
|
||||
# Install dependencies only when needed
|
||||
FROM node:16-alpine AS deps
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies based on the preferred package manager
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Next.js collects completely anonymous telemetry data about general usage.
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
#RUN yarn build
|
||||
|
||||
# If using npm comment out above and use below instead
|
||||
RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# You only need to copy next.config.js if you are NOT using the default configuration
|
||||
# COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
3
frontend/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Frontend for the social network
|
||||
|
||||
the frontend uses typscript + nextjs
|
22
frontend/components/comment.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function CommentBox({
|
||||
content,
|
||||
user,
|
||||
id
|
||||
}: {
|
||||
content: string
|
||||
user: string
|
||||
id: number
|
||||
}) {
|
||||
return (
|
||||
<div className="card w-96 card-side text-white hover:bg-base-300">
|
||||
<div className="card-body">
|
||||
<Link href={`/profile/${user}`}>
|
||||
<h2 className="hover:text-primary card-title">{user}</h2>
|
||||
</Link>
|
||||
<p className="text-xl">{content}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
27
frontend/components/post.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function PostBox({
|
||||
content,
|
||||
user,
|
||||
id
|
||||
}: {
|
||||
content: string
|
||||
user: string
|
||||
id: number
|
||||
}) {
|
||||
return (
|
||||
<Link href={`/post/${id}`}>
|
||||
<div className="card w-96 card-side text-white hover:bg-base-300">
|
||||
<div className="card-body">
|
||||
<Link href={`/profile/${user}`}>
|
||||
<h2 className="hover:text-primary card-title">{user}</h2>
|
||||
</Link>
|
||||
<p className="text-xl">{content}</p>
|
||||
<div className="card-actions justify-end">
|
||||
{/*<button className="btn btn-primary">Like</button>*/ }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
32
frontend/components/ui.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<ul className="menu bg-base-300 rounded-box fixed min-h-full py-6 space-y-9">
|
||||
<li>
|
||||
<Link href="/">
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/profile">
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<header className='grid place-items-center pt-5 pb-20'>
|
||||
<h1>Copybook</h1>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
5
frontend/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
15
frontend/next.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
nx: {
|
||||
svgr: false,
|
||||
},
|
||||
// fix for running the docker container
|
||||
output: 'standalone',
|
||||
experimental: {
|
||||
outputStandalone: true,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
5914
frontend/package-lock.json
generated
Normal file
27
frontend/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "social-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"daisyui": "^2.19.0",
|
||||
"next": "12.1.6",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"react-router-dom": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.40",
|
||||
"@types/react": "^18.0.11",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "8.17.0",
|
||||
"eslint-config-next": "12.1.6",
|
||||
"postcss": "^8.4.14",
|
||||
"tailwindcss": "^3.0.24"
|
||||
}
|
||||
}
|
6
frontend/pages/_app.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import '../styles/globals.css'
|
||||
import { AppProps } from 'next/app'
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
60
frontend/pages/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import PostBox from '../components/post';
|
||||
import Dashboard from '../components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
let url = process.env.NEXT_PUBLIC_URL;
|
||||
export default function Home(props) {
|
||||
const router = useRouter();
|
||||
let [posts, setPosts] = useState([]);
|
||||
useEffect(() => {
|
||||
let token = localStorage.getItem("Token");
|
||||
let ps = async () => {
|
||||
setPosts(await fetch(`http://${url}/posts`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
|
||||
}
|
||||
ps().catch(console.error);
|
||||
setInterval(refreshPosts, 20000)
|
||||
}, []);
|
||||
|
||||
if (posts["status"] == "bad") {
|
||||
localStorage.removeItem("Token");
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
let content = [];
|
||||
if (posts.length != 0 && posts["status"] == undefined) {
|
||||
content = Object.assign([], posts.map(post => {
|
||||
return [post.Content, post.ID, post.Username]
|
||||
}));
|
||||
content.reverse()
|
||||
}
|
||||
|
||||
let refreshPosts = async() => {
|
||||
let token = localStorage.getItem("Token");
|
||||
let ps = async () => {
|
||||
setPosts(await fetch(`http://${url}/posts`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
|
||||
}
|
||||
ps().catch(console.error);
|
||||
}
|
||||
|
||||
let newPost = async () => {
|
||||
let token = localStorage.getItem("Token");
|
||||
let y = document.getElementById("postContent") as HTMLInputElement;
|
||||
await fetch(`http://${url}/create`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, mode: "cors", body: JSON.stringify({ Title: "UI", Content: y.value }) });
|
||||
y.value = "";
|
||||
await refreshPosts();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dashboard />
|
||||
<main className='flex flex-col place-items-center space-y-3'>
|
||||
<textarea id="postContent" className="textarea textarea-secondary text-xl" placeholder="What's on your mind ?"></textarea>
|
||||
<button className="btn btn-outline btn-secondary w-20" onClick={newPost}>Post</button>
|
||||
{content.length != 0 ? content.map(post => {
|
||||
return <PostBox id={post[1]} content={post[0]} key={post[1]} user={post[2]}/>
|
||||
}) : ""}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
50
frontend/pages/login.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link'
|
||||
let url = process.env.NEXT_PUBLIC_URL
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
let stuff = localStorage.getItem("Token");
|
||||
if (stuff != null) {
|
||||
router.push("/")
|
||||
}
|
||||
}, []);
|
||||
|
||||
let grab_token = async (event) => {
|
||||
event.preventDefault();
|
||||
let username = (document.getElementById("Username") as HTMLInputElement).value;
|
||||
let password = (document.getElementById("Password") as HTMLInputElement).value;
|
||||
|
||||
let token = await fetch(`http://${url}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, mode: "cors", body: JSON.stringify({ "username": username, "pass": password }) }).then(r => r.json());
|
||||
if (token["status"] == "ok") {
|
||||
await localStorage.setItem("Token", token["text"]);
|
||||
router.push('/')
|
||||
}
|
||||
else {
|
||||
alert("bad username/password");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className='grid place-items-center pt-5 pb-20 text-white text-xl'>
|
||||
<h1>Copybook</h1>
|
||||
</header>
|
||||
|
||||
<form className="grid place-items-center space-y-5" action='#' onSubmit={grab_token}>
|
||||
<h1 className="text-xl pb-5">Login</h1>
|
||||
<input id="Username" type="text" placeholder="Username" className="input input-bordered input-accent w-full max-w-xs text-xl" />
|
||||
<input id="Password" type="password" placeholder="Password" className="input input-bordered input-primary w-full max-w-xs text-xl" />
|
||||
<button className="btn btn-outline btn-accent w-40">Login</button>
|
||||
<Link href="/signup">
|
||||
<a className="link link-accent">Signup ?</a>
|
||||
</Link>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
15
frontend/pages/logout.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export default function Logout() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
localStorage.removeItem("Token");
|
||||
router.push("/login");
|
||||
})
|
||||
return (
|
||||
<div>
|
||||
<h1>Logging you out ...</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
56
frontend/pages/post/[pid].tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Dashboard from '../../components/ui';
|
||||
import CommentBox from '../../components/comment';
|
||||
|
||||
export default function Profile() {
|
||||
const router = useRouter();
|
||||
|
||||
let pid = router.query.pid as string;
|
||||
let [post, setPost] = useState({text: ""});
|
||||
let [comments, setComments] = useState([]);
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return;
|
||||
let token = localStorage.getItem("Token");
|
||||
let ps = async () => {
|
||||
pid = router.query.pid as string;
|
||||
setPost(await fetch(`http://127.0.0.1:3000/posts/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
|
||||
setComments(await fetch(`http://127.0.0.1:3000/comments/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
|
||||
}
|
||||
ps().catch(console.error);
|
||||
}, [router.isReady]);
|
||||
|
||||
let refreshComments = () => {
|
||||
let token = localStorage.getItem("Token");
|
||||
let ps = async () => {
|
||||
setComments(await fetch(`http://127.0.0.1:3000/comments/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
|
||||
}
|
||||
ps().catch(console.error);
|
||||
}
|
||||
|
||||
let newComment = async () => {
|
||||
let token = localStorage.getItem("Token");
|
||||
let y = document.getElementById("commentContent") as HTMLInputElement;
|
||||
await fetch("http://127.0.0.1:3000/comment", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" }, mode: "cors", body: JSON.stringify({ "post_id": parseInt(pid), "content": y.value }) });
|
||||
y.value = "";
|
||||
refreshComments();
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Dashboard />
|
||||
<main className='flex flex-col place-items-center space-y-3'>
|
||||
<h1 className='text-2xl text-white'>{post.text}</h1>
|
||||
<textarea id="commentContent" className="textarea textarea-secondary text-xl" placeholder="New comment..."></textarea>
|
||||
<button className="btn btn-outline btn-secondary w-20" onClick={newComment}>Comment</button>
|
||||
<div className="divider"></div>
|
||||
<div className='pt-10'>
|
||||
{comments["Array"] != undefined ? comments["Array"].map(comment => {
|
||||
return (
|
||||
<CommentBox content={comment["Text"]} user={comment["Commenter"]} id={comment["PostId"]} />
|
||||
);
|
||||
}) : ""}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
27
frontend/pages/profile.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Dashboard from '../components/ui';
|
||||
let url = process.env.NEXT_PUBLIC_URL
|
||||
|
||||
export default function Profile() {
|
||||
const router = useRouter();
|
||||
|
||||
let [info, setInfo] = useState({});
|
||||
useEffect(() => {
|
||||
let token = localStorage.getItem("Token");
|
||||
let ps = async () => {
|
||||
setInfo(await fetch(`http://${url}/profile`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
|
||||
}
|
||||
ps().catch(console.error);
|
||||
}, [router.isReady]);
|
||||
return (
|
||||
<div>
|
||||
<Dashboard />
|
||||
<main className='flex flex-col place-items-center space-y-3'>
|
||||
<h1 className='text-2xl text-white'>{info["Username"]}</h1>
|
||||
<p>About me :</p>
|
||||
<p className='text-xl'>woops... </p>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
29
frontend/pages/profile/[pid].tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Dashboard from '../../components/ui';
|
||||
|
||||
|
||||
export default function Profile() {
|
||||
const router = useRouter();
|
||||
|
||||
let [info, setInfo] = useState({});
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return;
|
||||
let token = localStorage.getItem("Token");
|
||||
let ps = async () => {
|
||||
const { pid } = router.query
|
||||
setInfo(await fetch(`http://127.0.0.1:3000/profile/${pid}`, { headers: { "Authorization": `Bearer ${token}` }, mode: "cors" }).then(r => r.json()));
|
||||
}
|
||||
ps().catch(console.error);
|
||||
}, [router.isReady]);
|
||||
return (
|
||||
<div>
|
||||
<Dashboard />
|
||||
<main className='flex flex-col place-items-center space-y-3'>
|
||||
<h1 className='text-2xl text-white'>{info["Username"]}</h1>
|
||||
<p>About me :</p>
|
||||
<p className='text-xl'>woops... </p>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
38
frontend/pages/signup.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useRouter } from 'next/router';
|
||||
let url = process.env.NEXT_PUBLIC_URL
|
||||
|
||||
export default function Signup() {
|
||||
const router = useRouter();
|
||||
|
||||
let signup = async (event) => {
|
||||
event.preventDefault();
|
||||
let username = (document.getElementById("Username") as HTMLInputElement).value;
|
||||
let email = (document.getElementById("Email") as HTMLInputElement).value;
|
||||
let password = (document.getElementById("Password") as HTMLInputElement).value;
|
||||
|
||||
let token = await fetch(`http://${url}/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, mode: "cors", body: JSON.stringify({ "Username": username, "Email": email, "Password": password }) }).then(r => r.json());
|
||||
if (token["status"] == "ok") {
|
||||
router.push('/login')
|
||||
}
|
||||
else {
|
||||
alert("Username already exists");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<header className='grid place-items-center pt-5 pb-20 text-white text-xl'>
|
||||
<h1>Copybook</h1>
|
||||
</header>
|
||||
|
||||
<form className="grid place-items-center space-y-5" action="#" onSubmit={signup}>
|
||||
<h1 className="text-xl pb-5">Signup</h1>
|
||||
<input id="Email" type="text" placeholder="Email" className="input input-bordered input-accent w-full max-w-xs text-xl" />
|
||||
<input id="Username" type="text" placeholder="Username" className="input input-bordered input-accent w-full max-w-xs text-xl" />
|
||||
<input id="Password" type="password" placeholder="Password" className="input input-bordered input-primary w-full max-w-xs text-xl" />
|
||||
<button className="btn btn-outline btn-accent w-40">Signup</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
frontend/public/facebook_cover_photo_1.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/public/facebook_cover_photo_2.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
frontend/public/facebook_profile_image.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/favicon.png
Normal file
After Width: | Height: | Size: 268 B |
BIN
frontend/public/instagram_profile_image.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/public/linkedin_banner_image_1.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
frontend/public/linkedin_banner_image_2.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
frontend/public/linkedin_profile_image.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/public/logo.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/logo_transparent.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
frontend/public/pinterest_board_photo.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
frontend/public/pinterest_profile_image.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
frontend/public/twitter_header_photo_1.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
frontend/public/twitter_header_photo_2.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
frontend/public/twitter_profile_image.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
frontend/public/youtube_profile_image.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
116
frontend/styles/Home.module.css
Normal file
@ -0,0 +1,116 @@
|
||||
.container {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100vh;
|
||||
padding: 4rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 2rem 0;
|
||||
border-top: 1px solid #eaeaea;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a:hover,
|
||||
.title a:focus,
|
||||
.title a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 4rem 0;
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus,
|
||||
.card:active {
|
||||
color: #0070f3;
|
||||
border-color: #0070f3;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
20
frontend/styles/globals.css
Normal file
@ -0,0 +1,20 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
27
frontend/tailwind.config.js
Normal file
@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
mytheme: {
|
||||
"primary": "#e26394",
|
||||
"secondary": "#76e8f7",
|
||||
"accent": "#ed44a6",
|
||||
"neutral": "#16191D",
|
||||
"base-100": "#2F354C",
|
||||
"info": "#95AAE4",
|
||||
"success": "#12735F",
|
||||
"warning": "#F3DA58",
|
||||
"error": "#EB333F",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [require("daisyui")],
|
||||
}
|
30
frontend/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
22
nginx.conf
Normal file
@ -0,0 +1,22 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name social.org;
|
||||
location / {
|
||||
proxy_pass http://frontend:3000;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.social.org;
|
||||
location / {
|
||||
proxy_pass http://api:3000;
|
||||
}
|
||||
}
|
||||
}
|
3
remove-app.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo docker rmi social-network-nginx social-network-frontend
|
BIN
screenshots/1.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
screenshots/2.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
screenshots/3.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
screenshots/4.png
Normal file
After Width: | Height: | Size: 46 KiB |
3
start-app.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo docker compose up -d
|
3
stop-app.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo docker compose down
|