Flare-k

[Add and Modified] CSS and Database

$bgColor: red;
$red: #ea232c;
$dark-red: #bb2f2a;
$grey: #f5f5f5;
$black: #444444;
$dark-grey: #e7e7e7;
......
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
a {
all: unset;
cursor: pointer;
}
*,
input {
box-sizing: border-box;
}
input {
border: none;
box-sizing: border-box;
&:focus,
&:active {
outline: none;
}
}
html,
body {
height: 100%;
}
body {
background-color: #f5f5f5;
color: $black;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: 14px;
}
main {
width: 100%;
max-width: 1200px;
margin: 0 auto;
min-height: 70vh;
}
button,
input:not([type="file"]),
textarea,
.fileUpload {
padding: 7px 10px;
width: 100%;
border: none;
border-radius: 5px;
font-size: 14px;
color: $black;
font-weight: 600;
background-color: white;
max-width: 320px;
resize: none;
&::placeholder {
font-weight: 300;
color: rgba(0, 0, 0, 0.7);
}
}
button {
border: none;
background-color: #3498db;
color: white;
}
button.delete {
background-color: $dark-red;
}
button,
input[type="submit"] {
cursor: pointer;
}
.home-videos {
display: grid;
grid-template-columns: repeat(6, minmax(150px, 1fr));
grid-gap: 30px;
.videoBlock:first-child,
.videoBlock:nth-child(2) {
grid-column: span 3;
}
.videoBlock:nth-child(3),
.videoBlock:nth-child(4),
.videoBlock:nth-child(5) {
grid-column: span 2;
}
}
.video-detail__container {
display: flex;
flex-direction: column;
align-items: center;
.video__info {
width: 100%;
max-width: 850px;
margin-top: 25px;
button {
width: 100px;
margin-bottom: 25px;
}
.video__title,
.video__views,
.video__description {
display: block;
margin-bottom: 15px;
}
.video__title {
font-size: 22px;
font-weight: 300;
}
.video__views {
font-size: 14px;
}
.video__description {
font-size: 16px;
}
}
.video__comments {
margin-top: 25px;
.video__comment-number {
font-size: 18px;
}
}
}
.footer {
margin: 50px 0;
padding-top: 50px;
border-top: 3px solid rgba(0, 0, 0, 0.1);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
.footer__icon {
color: rgba(0, 0, 0, 0.2);
font-size: 40px;
margin-bottom: 20px;
}
.footer__text {
color: rgba(0, 0, 0, 0.2);
font-weight: 800;
text-transform: uppercase;
}
}
.form-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
form {
width: 100%;
max-width: 320px;
margin-bottom: 50px;
input:not([type="file"]),
.fileUpload {
display: block;
width: 100%;
padding-top: 10px;
padding-bottom: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
&:not(:last-child) {
margin-bottom: 25px;
}
}
textarea {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
margin-bottom: 25px;
}
input[type="submit"] {
background-color: #3498db;
color: white;
}
}
a {
max-width: 320px;
width: 100%;
}
.fileUpload {
label {
font-weight: 300;
margin-right: 10px;
}
}
}
.header {
background-color: $red;
margin-bottom: 50px;
.header__wrapper {
padding: 5px 0px;
width: 100%;
margin: 0 auto;
max-width: 1200px;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
.header__column {
i {
color: white;
font-size: 30px;
}
&:nth-child(2) {
width: 100%;
justify-self: center;
}
&:last-child {
justify-self: end;
}
ul {
display: flex;
color: white;
font-weight: 600;
text-transform: uppercase;
a {
color: inherit;
text-decoration: none;
}
li:not(:last-child) {
margin-right: 15px;
}
}
form {
width: 100%;
input {
padding: 7px 10px;
width: 100%;
border-radius: 5px;
font-size: 14px;
color: $black;
font-weight: 600;
max-width: none;
&::placeholder {
font-weight: 300;
color: rgba(0, 0, 0, 0.7);
}
}
}
}
}
}
.social-login {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
button {
width: 100%;
max-width: 320px;
display: flex;
justify-content: center;
align-items: center;
&:not(:last-child) {
margin-bottom: 15px;
}
span {
margin-right: 10px;
font-size: 20px;
}
}
.social-login--github {
background-color: $black;
color: white;
}
.social-login--facebook {
background-color: #3a5998;
color: white;
}
}
.videoBlock {
video {
margin-bottom: 10px;
max-width: 100%;
}
.videoBlock__title {
font-size: 18px;
font-weight: 300;
margin-bottom: 10px;
}
}
@import "config/variables";
body {
background-color: $bgColor;
}
@import "config/_variables.scss";
@import "config/reset.scss";
@import "main.scss";
@import "partials/header.scss";
@import "partials/footer.scss";
@import "partials/form.scss";
@import "partials/socialLogin.scss";
@import "partials/videoBlock.scss";
@import "pages/home.scss";
@import "pages/videoDetail.scss";
......
......@@ -2,41 +2,40 @@ import routes from "../routes";
// 회원가입 -> 완료 -> 홈화면으로 Redirect
export const getJoin = (req, res) => {
res.render("join", { pageTitle: "Join" });
res.render("join", { pageTitle: "Join" });
};
export const postJoin = (req, res) => {
const {
body: { name, email, password, password2 },
} = req;
if (password !== password2) {
res.status(400);
res.render("join", { pageTitle: "Join" });
} else {
// To Do: Register User
// To Do: Log user in
res.redirect(routes.home);
}
const {
body: { name, email, password, password2 },
} = req;
if (password !== password2) {
res.status(400);
res.render("join", { pageTitle: "Join" });
} else {
// To Do: Register User
// To Do: Log user in
res.redirect(routes.home);
}
};
export const getLogin = (req, res) =>
res.render("login", { pageTitle: "Login" });
res.render("login", { pageTitle: "Login" });
export const postLogin = (req, res) => {
res.redirect(routes.home);
res.redirect(routes.home);
};
//로그아웃을 클릭하면 LogOut페이지로 가는 것 대신에, 로그아웃을 처리한 후
// 로그아웃을 클릭하면 LogOut페이지로 가는 것 대신에, 로그아웃을 처리한 후
// home 페이지로 Redirect로 표현할 것이다.
//즉, 초반에 만들어둔 logout.pug는 삭제해도 좋다.
// 즉, 초반에 만들어둔 logout.pug는 삭제해도 좋다.
export const logout = (req, res) => {
//res.render("logout", { pageTitle: "Logout" });
res.redirect(routes.home);
// res.render("logout", { pageTitle: "Logout" });
res.redirect(routes.home);
};
export const users = (req, res) => res.render("users", { pageTitle: "Users" });
export const userDetail = (req, res) =>
res.render("userDetail", { pageTitle: "User Detail" });
res.render("userDetail", { pageTitle: "User Detail" });
export const editProfile = (req, res) =>
res.render("editProfile", { pageTitle: "Edit Profile" });
res.render("editProfile", { pageTitle: "Edit Profile" });
export const changePassword = (req, res) =>
res.render("changePassword", { pageTitle: "Change Password" });
\ No newline at end of file
res.render("changePassword", { pageTitle: "Change Password" });
......
......@@ -86,7 +86,7 @@ export const postEditVideo = async (req, res) => {
try {
// id를 찾아서 body를 얻어와야 한다. 비디오 수정에서 제목과 설명을 가져와야 하기 때문이다.
// mongoose엔 우리의 id가 없어서 _id : id로 찾아줘야 한다.
await Video.findOneAndUpdate({ _id: id }, { title, description }); //title:title == title
await Video.findOneAndUpdate({ _id: id }, { title, description }); // title:title == title
// 이렇게 하면 default로 얻어온 제목 및 내용을 수정하여 form을 전송하면 해당 내용으로 업데이트 된다.
res.redirect(routes.videoDetail(id));
} catch (error) {
......
import dotenv from "dotenv";
import app from "./app"; // app.js에서 export default app했기 때문에 불러올 수 있다.
import "./db";
import dotenv from "dotenv";
dotenv.config();
import "./models/Video";
import "./models/Comment";
dotenv.config();
const PORT = process.env.PORT || 80;
const handleListening = () => {
console.log(`✅ Listening on: http://localhost:${PORT}`);
//call-back함수.
//PORT를 listen하기 시작할 때 함수를 호출해준다.
console.log(`✅ Listening on: http://localhost:${PORT}`);
// call-back함수.
// PORT를 listen하기 시작할 때 함수를 호출해준다.
};
app.listen(PORT, handleListening);
\ No newline at end of file
app.listen(PORT, handleListening);
......
......@@ -4,14 +4,14 @@ import routes from "./routes";
const multerVideo = multer({ dest: "uploads/videos/" });
export const localsMiddleware = (req, res, next) => {
res.locals.siteName = "my Youtube";
res.locals.routes = routes;
res.locals.user = {
isAuthenticated: true,
id: 1,
};
next();
res.locals.siteName = "my Youtube";
res.locals.routes = routes;
res.locals.user = {
isAuthenticated: false,
id: 1,
};
next();
};
export const uploadVideo = multerVideo.single("videoFile");
//single에 들어간 videoFile은 upload.pug의 file 부분 input name
\ No newline at end of file
// single에 들어간 videoFile은 upload.pug의 file 부분 input name
......
import mongoose from "mongoose";
//video 댓글에 대한 Database
// video 댓글에 대한 Database
const CommentSchema = new mongoose.Schema({
text: {
type: String,
required: "Text is required"
}, //이러한 형태를 configuration object라 한다.
createdAt: {
type: Date,
default: Date.now
}
/*
,
text: {
type: String,
required: "Text is required",
}, // 이러한 형태를 configuration object라 한다.
createdAt: {
type: Date,
default: Date.now,
},
/*
video: { //video와 comment를 연결하는 방법 #2
type: mongoose.Schema.Types.ObjectId, //그 다음 어느 model에서 온 id인지 알려줘야 한다.
ref: "Video"
}*/
}
*/
});
const model = mongoose.model("Comment", CommentSchema);
export default model;
\ No newline at end of file
export default model;
......
//DB 모델을 작성한다.
//Video 자체를 DB에 저장하진 않을 것이다. 즉, byte를 저장하는 것이 아니라 video의 link를 저장한다.
// DB 모델을 작성한다.
// Video 자체를 DB에 저장하진 않을 것이다. 즉, byte를 저장하는 것이 아니라 video의 link를 저장한다.
import mongoose from "mongoose";
const VideoSchema = new mongoose.Schema({
fileUrl: {
type: String,
required: "File URL is required" //url이 없으면 오류메시지 출력
fileUrl: {
type: String,
required: "File URL is required", // url이 없으면 오류메시지 출력
},
title: {
type: String,
required: "Title is required",
},
description: String,
views: {
type: Number,
default: 0, // 비디오를 처음 생성하면 views를 0으로..
},
createdAt: {
type: Date,
default: Date.now, // 현재 날짜를 반환하는 function
},
// video와 comment를 연결하는 방법 #1
comments: [
{
type: mongoose.Schema.Types.ObjectId, // 그 다음 어느 model에서 온 id인지 알려줘야 한다.
ref: "Comment",
},
title: {
type: String,
required: "Title is required"
},
description: String,
views: {
type: Number,
default: 0 //비디오를 처음 생성하면 views를 0으로..
},
createdAt: {
type: Date,
default: Date.now //현재 날짜를 반환하는 function
},
//video와 comment를 연결하는 방법 #1
comments: [{
type: mongoose.Schema.Types.ObjectId, //그 다음 어느 model에서 온 id인지 알려줘야 한다.
ref: "Comment"
}]
],
});
// 이제 이 스키마를 이용하여 model을 만들어준다.
//모델의 이름은 "Video"
// 모델의 이름은 "Video"
const model = mongoose.model("Video", VideoSchema);
export default model;
//모델이 만들어짐을 알리기 위해 init.js에 import해준다.
\ No newline at end of file
// 모델이 만들어짐을 알리기 위해 init.js에 import해준다.
......
......@@ -4,7 +4,7 @@
"description": "make Youtube Website",
"main": "app.js",
"scripts": {
"dev:server": "nodemon --exec babel-node init.js --delay 2",
"dev:server": "nodemon --exec babel-node init.js --delay 2 --ignore 'scss'",
"dev:assets": "WEBPACK_ENV=development webpack -w",
"build:assets": "WEBPACK_ENV=production webpack"
},
......
import express from "express";
import routes from "../routes";
import {
getUpload,
postUpload,
videoDetail,
getEditVideo,
postEditVideo,
deleteVideo,
getUpload,
postUpload,
videoDetail,
getEditVideo,
postEditVideo,
deleteVideo,
} from "../controllers/videoController";
import { uploadVideo } from "../middlewares";
//export const videoRouter = express.Router(); 이렇게하면 이 변수만 export하게 된다.
// export const videoRouter = express.Router(); 이렇게하면 이 변수만 export하게 된다.
const videoRouter = express.Router();
//Upload
// Upload
videoRouter.get(routes.upload, getUpload);
videoRouter.post(routes.upload, uploadVideo, postUpload);
......@@ -26,4 +26,4 @@ videoRouter.post(routes.editVideo(), postEditVideo);
// Video Delete
videoRouter.get(routes.deleteVideo(), deleteVideo);
export default videoRouter;
\ No newline at end of file
export default videoRouter;
......
......@@ -19,44 +19,44 @@ const EDIT_VIDEO = "/:id/edit";
const DELETE_VIDEO = "/:id/delete";
const routes = {
home: HOME,
join: JOIN,
login: LOGIN,
logout: LOGOUT,
search: SEARCH,
users: USERS,
userDetail: (id) => {
if (id) {
return `/users/${id}`;
} else {
return USER_DETAIL;
}
},
editProfile: EDIT_PROFILE,
changePassword: CHANGE_PASSWORD,
videos: VIDEOS,
upload: UPLOAD,
videoDetail: (id) => {
if (id) {
return `/videos/${id}`;
} else {
return VIDEO_DETAIL;
}
},
editVideo: (id) => {
if (id) {
return `/videos/${id}/edit`;
} else {
return EDIT_VIDEO;
}
},
deleteVideo: (id) => {
if (id) {
return `/videos/${id}/delete`;
} else {
return DELETE_VIDEO;
}
},
home: HOME,
join: JOIN,
login: LOGIN,
logout: LOGOUT,
search: SEARCH,
users: USERS,
userDetail: (id) => {
if (id) {
return `/users/${id}`;
} else {
return USER_DETAIL;
}
},
editProfile: EDIT_PROFILE,
changePassword: CHANGE_PASSWORD,
videos: VIDEOS,
upload: UPLOAD,
videoDetail: (id) => {
if (id) {
return `/videos/${id}`;
} else {
return VIDEO_DETAIL;
}
},
editVideo: (id) => {
if (id) {
return `/videos/${id}/edit`;
} else {
return EDIT_VIDEO;
}
},
deleteVideo: (id) => {
if (id) {
return `/videos/${id}/delete`;
} else {
return DELETE_VIDEO;
}
},
};
export default routes;
\ No newline at end of file
export default routes;
......
This diff is collapsed. Click to expand it.
......@@ -3,9 +3,11 @@ extends layouts/main
block content
.form-container
form(action=routes.editProfile, method="post")
label(for="avatar") Avatar
input(type="file", id="avatar", name="avatar")
.fileUpload
label(for="avatar") Avatar
input(type="file", id="avatar", name="avatar")
input(type="text", placeholder="Name", name="name")
input(type="email", placeholder="Email", name="email")
input(type="submit", value="Update Profile")
a.form-container__link(href=`/users${routes.changePassword}`) Change Password
\ No newline at end of file
a.form-container__link(href=`/users${routes.changePassword}`)
button Change Password
\ No newline at end of file
......
......@@ -6,4 +6,5 @@ block content
input(type="text", placeholder="Title", name="title", value=video.title)
textarea(name="description", placeholder="Description")=video.description
input(type="submit", value="Update Video")
a.form-container__link.form-container__link--delete(href=routes.deleteVideo(video.id)) Delete Video
\ No newline at end of file
a.form-container__link(href=routes.deleteVideo(video.id))
button.delete Delete Video
\ No newline at end of file
......
......@@ -2,11 +2,77 @@ extends layouts/main
include mixins/videoBlock
block content
.videos
.home-videos
each item in videos
+videoBlock({
id: item.id,
title : item.title,
views: item.views,
videoFile:item.fileUrl
})
\ No newline at end of file
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
+videoBlock({
id:item.id,
title:item.title,
views:item.views,
videoFile:item.fileUrl
})
......
......@@ -3,5 +3,8 @@ mixin videoBlock(video = {})
a(href=routes.videoDetail(video.id))
video.videoBlock__thumbnail(src=video.videoFile, controls=true)
h4.videoBlock__title=video.title
h6.videoBlock__views=video.views
if video.views === 1
h6.videoBlock__views 1 view
else
h6.videoBlock__views #{video.views} views
\ No newline at end of file
......
header.header
.header__column
a(href=routes.home)
i.fab.fa-youtube
.header__column
form(action=routes.search, method="get")
input(type="text", placeholder="Search by term...", name="term")
.header__column
ul
if !user.isAuthenticated
li
a(href=routes.join) Join
li
a(href=routes.login) Log In
else
li
a(href=`/videos${routes.upload}`) Upload
li
a(href=routes.userDetail(user.id)) Profile
li
a(href=routes.logout) Log out
.header__wrapper
.header__column
a(href=routes.home)
i.fab.fa-youtube
.header__column
form(action=routes.search, method="get")
input(type="text", placeholder="Search by term...", name="term")
.header__column
ul
if !user.isAuthenticated
li
a(href=routes.join) Join
li
a(href=routes.login) Log In
else
li
a(href=`/videos${routes.upload}`) Upload
li
a(href=routes.userDetail(user.id)) Profile
li
a(href=routes.logout) Log out
......
......@@ -11,7 +11,6 @@ block content
+videoBlock({
title : item.title,
views: item.views,
videoFile:item.videoFile
videoFile:item.videoFile,
id: item.id
})
\ No newline at end of file
......
......@@ -3,8 +3,9 @@ extends layouts/main
block content
.form-container
form(action=`/videos${routes.upload}`, method="post", enctype="multipart/form-data")
label(for="file") Video File
input(type="file", id="file", name="videoFile", required=true, accept = "video/*")
div.fileUpload
label(for="file") Video File
input(type="file", id="file", name="videoFile", required=true, accept = "video/*")
input(type="text", placeholder="Title", name="title", required=true)
textarea(name="description", placeholder="Description", required=true)
input(type="submit", value="Upload Video")
\ No newline at end of file
......
......@@ -4,10 +4,15 @@ block content
.video__player
video(src=`/${video.fileUrl}`)
.video__info
a(href=routes.editVideo(video.id)) Edit video
a(href=routes.editVideo(video.id))
button Edit video
h5.video__title=video.title
span.video__views=video.views
p.video__description=video.description
if video.views === 1
span.video__views 1 view
else
span.video__views #{video.views} views
.video__comment
if video.comments.length === 1
span.video__comment-number 1 comment
......