functional version
This commit is contained in:
parent
00288613fc
commit
b2d56b7dfd
20 changed files with 2834 additions and 0 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
13
index.html
Normal file
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1823
package-lock.json
generated
Normal file
1823
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "vue-blog-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.30",
|
||||||
|
"sass": "^1.68.0",
|
||||||
|
"tailwindcss": "^3.3.3",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
21
src/App.vue
Normal file
21
src/App.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup>
|
||||||
|
import { loading, isAuthenticated } from "./auth.js";
|
||||||
|
import AuthenticatedLayout from "./components/AuthenticatedLayout.vue";
|
||||||
|
import GuestLayout from "./components/GuestLayout.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen w-full bg-zinc-900 text-gray-100 flex gap-4 font-mono"
|
||||||
|
>
|
||||||
|
<div class="flex w-full justify-center items-center" v-if="loading">
|
||||||
|
<p class="text-3xl loading">
|
||||||
|
Loading<span>.</span><span>.</span><span>.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<main class="flex flex-col flex-1" v-else>
|
||||||
|
<AuthenticatedLayout v-if="isAuthenticated" />
|
||||||
|
<GuestLayout v-else />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
29
src/auth.js
Normal file
29
src/auth.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
// make two references
|
||||||
|
const loading = ref(true);
|
||||||
|
const isAuthenticated = ref(false);
|
||||||
|
const currentUser = ref(null);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
// check authentication
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/ping`, options)
|
||||||
|
.then((res) => {
|
||||||
|
// once loaded, set loading to false
|
||||||
|
loading.value = false;
|
||||||
|
// if auth ok, set isAuthenticated to true
|
||||||
|
if (res.ok) {
|
||||||
|
isAuthenticated.value = true;
|
||||||
|
console.log(isAuthenticated);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
if (json) {
|
||||||
|
currentUser.value = json.user.username;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { loading, isAuthenticated, currentUser };
|
||||||
33
src/components/AuthenticatedLayout.vue
Normal file
33
src/components/AuthenticatedLayout.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import Logo from "./Logo.vue";
|
||||||
|
import { currentUser } from "../auth.js";
|
||||||
|
|
||||||
|
// make ref for title
|
||||||
|
const title = ref("");
|
||||||
|
const username = ref(currentUser);
|
||||||
|
|
||||||
|
// change title dynamically based on emit
|
||||||
|
const changeTitle = (text) => {
|
||||||
|
title.value = text;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="flex items-center justify-between w-full border-b">
|
||||||
|
<Logo />
|
||||||
|
<div class="grow flex justify-center text-2xl">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<router-link :to="`/user/${username}`">
|
||||||
|
<div
|
||||||
|
class="h-[4rem] w-[8rem] px-5 flex justify-center items-center border-l hover:bg-zinc-100 hover:text-zinc-700"
|
||||||
|
>
|
||||||
|
<h1 class="text-lg">🥷 {{ username }}</h1>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</nav>
|
||||||
|
<div class="h-full flex justify-center">
|
||||||
|
<router-view @title="(title) => changeTitle(title)"></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
38
src/components/GuestLayout.vue
Normal file
38
src/components/GuestLayout.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import Logo from "./Logo.vue";
|
||||||
|
|
||||||
|
// make ref for title
|
||||||
|
const title = ref("");
|
||||||
|
|
||||||
|
// change title dynamically based on emit
|
||||||
|
const changeTitle = (text) => {
|
||||||
|
title.value = text;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="flex items-center justify-between w-full border-b">
|
||||||
|
<Logo />
|
||||||
|
<div class="grow flex justify-center text-2xl">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<router-link to="/login">
|
||||||
|
<div
|
||||||
|
class="h-[4rem] w-[8rem] px-5 flex justify-center items-center border-l border-r hover:bg-cyan-700"
|
||||||
|
>
|
||||||
|
<h1 class="text-lg">Log In</h1>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/user/new_user">
|
||||||
|
<div
|
||||||
|
class="h-[4rem] w-[8rem] max-w-[8rem] px-5 flex justify-center items-center hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
<h1 class="text-lg">Sign Up</h1>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</nav>
|
||||||
|
<div class="h-full flex justify-center">
|
||||||
|
<router-view @title="(title) => changeTitle(title)"></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
9
src/components/Logo.vue
Normal file
9
src/components/Logo.vue
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<router-link to="/">
|
||||||
|
<div
|
||||||
|
class="h-[4rem] px-5 flex items-center border-r hover:bg-gray-100 hover:text-zinc-900"
|
||||||
|
>
|
||||||
|
<h1 class="font-mono text-2xl">myPlace()</h1>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
13
src/main.js
Normal file
13
src/main.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as Vue from "vue";
|
||||||
|
import * as VueRouter from "vue-router";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import { routes } from "./router/index.js";
|
||||||
|
import "./styles/style.scss";
|
||||||
|
|
||||||
|
// set up router
|
||||||
|
const router = VueRouter.createRouter({
|
||||||
|
history: VueRouter.createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.createApp(App).use(router).mount("#app");
|
||||||
13
src/router/index.js
Normal file
13
src/router/index.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Home from "../views/Home.vue";
|
||||||
|
import Post from "../views/Post.vue";
|
||||||
|
import Login from "../views/Login.vue";
|
||||||
|
import Signup from "../views/Signup.vue";
|
||||||
|
import User from "../views/User.vue";
|
||||||
|
|
||||||
|
export const routes = [
|
||||||
|
{ path: "/", component: Home },
|
||||||
|
{ path: "/post/:postID", component: Post },
|
||||||
|
{ path: "/login", component: Login },
|
||||||
|
{ path: "/user/new_user", component: Signup },
|
||||||
|
{ path: "/user/:userID", component: User },
|
||||||
|
];
|
||||||
73
src/styles/style.scss
Normal file
73
src/styles/style.scss
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading:after {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
-webkit-animation: ellipsis steps(4, end) 1800ms infinite;
|
||||||
|
animation: ellipsis steps(4, end) 1800ms infinite;
|
||||||
|
content: "\2026"; /* ascii code for the ellipsis character */
|
||||||
|
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ellipsis {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading span {
|
||||||
|
animation-name: ellipsis;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-duration: 2.8s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@for $i from 2 through 3 {
|
||||||
|
.loading span:nth-child(#{$i}) {
|
||||||
|
animation-delay: #{0 + (($i - 1) * 0.7)}s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
171
src/views/Home.vue
Normal file
171
src/views/Home.vue
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick } from "vue";
|
||||||
|
import { isAuthenticated } from "../auth.js";
|
||||||
|
|
||||||
|
// make references
|
||||||
|
const loading = ref(true);
|
||||||
|
const posts = ref([]);
|
||||||
|
const emit = defineEmits(["title"]);
|
||||||
|
const hideForm = ref(true);
|
||||||
|
|
||||||
|
// updates component
|
||||||
|
const update = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
await nextTick();
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// if page needs reloading, reload here
|
||||||
|
if (localStorage.getItem("needsReload")) {
|
||||||
|
// clear from local storage
|
||||||
|
localStorage.removeItem("needsReload");
|
||||||
|
// reload page
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// get posts
|
||||||
|
const getPosts = async () => {
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/`)
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
loading.value = false;
|
||||||
|
const arr = [];
|
||||||
|
json.posts.forEach((post) => {
|
||||||
|
if (post.published == true) arr.push(post);
|
||||||
|
});
|
||||||
|
posts.value = arr;
|
||||||
|
emit("title", "");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getPosts();
|
||||||
|
|
||||||
|
const submitPost = async (title, text) => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
published: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// send to backend
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/post/new_post`, options).then(
|
||||||
|
(res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
// fetch new posts
|
||||||
|
getPosts();
|
||||||
|
// hide comment form
|
||||||
|
hideForm.value = true;
|
||||||
|
// update view
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePost = async (title, text) => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
published: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// send to backend
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/post/new_post`, options).then(
|
||||||
|
(res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
// fetch new posts
|
||||||
|
getPosts();
|
||||||
|
// hide comment form
|
||||||
|
hideForm.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
class="h-full flex flex-col justify-center items-center"
|
||||||
|
v-if="loading"
|
||||||
|
>
|
||||||
|
<p class="text-3xl loading">
|
||||||
|
Loading<span>.</span><span>.</span><span>.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-col" v-else v-for="post in posts">
|
||||||
|
<router-link
|
||||||
|
:to="`/post/${post._id}`"
|
||||||
|
class="flex flex-col items-center w-full h-fit border-b hover:bg-zinc-700 py-10"
|
||||||
|
>
|
||||||
|
<h1 class="text-2xl mb-4">
|
||||||
|
『 {{ post.title }} 』
|
||||||
|
</h1>
|
||||||
|
<p class="mb-4">
|
||||||
|
{{ post.author }} 🖊️ {{ post.date.substring(0, 10) }}
|
||||||
|
</p>
|
||||||
|
<p>{{ post.text }}</p>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="w-3/4 flex justify-center my-10" v-if="isAuthenticated">
|
||||||
|
<button
|
||||||
|
v-if="hideForm"
|
||||||
|
@click="hideForm = false"
|
||||||
|
class="hover:bg-emerald-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
+ Post
|
||||||
|
</button>
|
||||||
|
<div class="flex flex-col align-center w-full items-center" v-else>
|
||||||
|
<div class="flex items-center justify-center mb-10 gap-10">
|
||||||
|
<label for="post_title">Title:</label>
|
||||||
|
<input
|
||||||
|
id="post_title"
|
||||||
|
class="bg-zinc-900 border p-5"
|
||||||
|
v-model="postTitle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="postText"
|
||||||
|
class="bg-zinc-900 border p-5 mb-10 w-2/3"
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
<div class="flex items-center justify-center gap-10">
|
||||||
|
<button
|
||||||
|
@click="submitPost(postTitle, postText)"
|
||||||
|
class="hover:bg-emerald-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
🚀 Publish
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="savePost(postTitle, postText)"
|
||||||
|
class="hover:bg-yellow-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
💾 Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
59
src/views/Login.vue
Normal file
59
src/views/Login.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
// emit title
|
||||||
|
const emit = defineEmits(["title"]);
|
||||||
|
emit("title", "Log In");
|
||||||
|
|
||||||
|
// attempt login
|
||||||
|
const attemptLogin = (username, password) => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
// send to backend
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/login`, options).then((res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
// indicate that a reload is needed
|
||||||
|
localStorage.setItem("needsReload", "true");
|
||||||
|
router.push("/");
|
||||||
|
} else {
|
||||||
|
alert("Incorrect username or password!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-10">
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input id="username" class="bg-zinc-900 border p-5" v-model="username" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<label for="username">Password:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="username"
|
||||||
|
class="bg-zinc-900 border p-5"
|
||||||
|
v-model="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="attemptLogin(username, password)"
|
||||||
|
class="mt-2 hover:bg-emerald-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
🔑 Log In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
238
src/views/Post.vue
Normal file
238
src/views/Post.vue
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick } from "vue";
|
||||||
|
import { currentUser, isAuthenticated } from "../auth.js";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const emit = defineEmits(["title"]);
|
||||||
|
|
||||||
|
// make references
|
||||||
|
const loading = ref(true);
|
||||||
|
const post = ref(null);
|
||||||
|
const isAuthor = ref(false);
|
||||||
|
const isPublished = ref(true);
|
||||||
|
const showCommentForm = ref(false);
|
||||||
|
|
||||||
|
// updates component
|
||||||
|
const update = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
await nextTick();
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// get post
|
||||||
|
const getPost = async () => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/post/${route.params.postID}`,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.ok) return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
emit("title", json.post.title);
|
||||||
|
if (json.post.author == currentUser.value) {
|
||||||
|
isAuthor.value = true;
|
||||||
|
}
|
||||||
|
json.post.published
|
||||||
|
? (isPublished.value = true)
|
||||||
|
: (isPublished.value = false);
|
||||||
|
loading.value = false;
|
||||||
|
post.value = json.post;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getPost();
|
||||||
|
|
||||||
|
// submit comment
|
||||||
|
const submitComment = async (text, name) => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
author: name,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// send to backend
|
||||||
|
fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/post/${
|
||||||
|
route.params.postID
|
||||||
|
}/new_comment`,
|
||||||
|
options
|
||||||
|
).then((res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
// fetch new posts
|
||||||
|
getPost();
|
||||||
|
// hide comment form
|
||||||
|
showCommentForm.value = false;
|
||||||
|
// update view
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const togglePublish = async (bool) => {
|
||||||
|
let options;
|
||||||
|
if (bool) {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
options = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: post._rawValue.title,
|
||||||
|
text: post._rawValue.text,
|
||||||
|
published: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
options = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: post._rawValue.title,
|
||||||
|
text: post._rawValue.text,
|
||||||
|
published: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// send to backend
|
||||||
|
fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/post/${route.params.postID}`,
|
||||||
|
options
|
||||||
|
).then((res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
// fetch new posts
|
||||||
|
getPost();
|
||||||
|
// update view
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePost = async () => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/post/${route.params.postID}`,
|
||||||
|
options
|
||||||
|
).then((res) => {
|
||||||
|
if (res.ok) router.push("/");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full flex justify-center">
|
||||||
|
<div class="flex flex-col justify-center" v-if="loading">
|
||||||
|
<p class="text-3xl loading">
|
||||||
|
Loading<span>.</span><span>.</span><span>.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-col items-center h-fit" v-else>
|
||||||
|
<div class="w-full flex flex-col items-center py-10">
|
||||||
|
<p class="mb-4">{{ post.text }}</p>
|
||||||
|
<p>
|
||||||
|
{{ post.author }} 🖊️ {{ post.date.substring(0, 10) }}
|
||||||
|
</p>
|
||||||
|
<div v-if="isAuthor" class="mt-10 flex items-center gap-10">
|
||||||
|
<router-link
|
||||||
|
to="/post/new_post"
|
||||||
|
class="hover:bg-zinc-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>🖊️ Edit</router-link
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="isPublished"
|
||||||
|
@click="togglePublish(false)"
|
||||||
|
class="hover:bg-yellow-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
@click="togglePublish(true)"
|
||||||
|
class="hover:bg-yellow-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deletePost()"
|
||||||
|
class="hover:bg-red-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
🚮 Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-2/3 flex flex-col items-center justify-center mb-10 py-10 border"
|
||||||
|
v-for="comment in post.comments"
|
||||||
|
>
|
||||||
|
<p class="mb-4">{{ comment.text }}</p>
|
||||||
|
<p>
|
||||||
|
{{ comment.author }} 🖊️
|
||||||
|
{{ comment.date.substring(0, 10) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center w-2/3 mb-10">
|
||||||
|
<button
|
||||||
|
v-if="showCommentForm == false"
|
||||||
|
@click="showCommentForm = true"
|
||||||
|
class="hover:bg-emerald-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
+ Comment
|
||||||
|
</button>
|
||||||
|
<div class="flex flex-col align-center w-full" v-else>
|
||||||
|
<textarea
|
||||||
|
v-model="commentText"
|
||||||
|
class="bg-zinc-900 border p-5 mb-10"
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
<div class="flex items-center justify-center gap-10">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center"
|
||||||
|
v-if="!isAuthenticated"
|
||||||
|
>
|
||||||
|
<label for="comment_author">Your name: </label>
|
||||||
|
<input
|
||||||
|
id="comment_author"
|
||||||
|
class="bg-zinc-900 border p-5"
|
||||||
|
v-model="commentAuthor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="submitComment(commentText, commentAuthor)"
|
||||||
|
class="hover:bg-emerald-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
✅ Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
64
src/views/Signup.vue
Normal file
64
src/views/Signup.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// emit title
|
||||||
|
const emit = defineEmits(["title"]);
|
||||||
|
emit("title", "Sign Up");
|
||||||
|
|
||||||
|
// create user
|
||||||
|
const createUser = (username, password) => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
// send to backend
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/user/new_user`, options).then(
|
||||||
|
(res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
// automatically login for new user
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/login`, options).then(() => {
|
||||||
|
// indicate that a reload is needed
|
||||||
|
localStorage.setItem("needsReload", "true");
|
||||||
|
router.push("/");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert("There were errors! Please try again!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-10">
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input id="username" class="bg-zinc-900 border p-5" v-model="username" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<label for="username">Password:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="username"
|
||||||
|
class="bg-zinc-900 border p-5"
|
||||||
|
v-model="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="createUser(username, password)"
|
||||||
|
class="mt-2 hover:bg-emerald-700 py-5 w-[8rem] flex border justify-center"
|
||||||
|
>
|
||||||
|
🙋 Sign Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
167
src/views/User.vue
Normal file
167
src/views/User.vue
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { currentUser } from "../auth.js";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// emit title
|
||||||
|
const emit = defineEmits(["title"]);
|
||||||
|
emit("title", currentUser._rawValue);
|
||||||
|
|
||||||
|
const posts = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const hideForm = ref(true);
|
||||||
|
|
||||||
|
// get posts
|
||||||
|
const getPosts = async () => {
|
||||||
|
const options = {
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/`, options)
|
||||||
|
.then((res) => {
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
loading.value = false;
|
||||||
|
const arr = [];
|
||||||
|
json.posts.forEach((post) => {
|
||||||
|
if (post.author == currentUser._rawValue) arr.push(post);
|
||||||
|
});
|
||||||
|
posts.value = arr;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getPosts();
|
||||||
|
|
||||||
|
// edit user
|
||||||
|
const editUser = (username, password) => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
let options = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
// send to backend
|
||||||
|
fetch(
|
||||||
|
`${import.meta.env.VITE_BACKEND_URL}/user/${currentUser._rawValue}`,
|
||||||
|
options
|
||||||
|
).then((res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
const logoutOpts = {
|
||||||
|
method: "GET",
|
||||||
|
};
|
||||||
|
// logout
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/logout`, logoutOpts).then(
|
||||||
|
() => {
|
||||||
|
options.method = "POST";
|
||||||
|
// then automatically re-login for new user
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/login`, options).then(
|
||||||
|
() => {
|
||||||
|
localStorage.setItem("needsReload", "true");
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alert("There were errors! Please try again!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// logout
|
||||||
|
const logout = () => {
|
||||||
|
// create JSON object and proper headers
|
||||||
|
const options = {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
// send to backend
|
||||||
|
fetch(`${import.meta.env.VITE_BACKEND_URL}/logout`, options).then((res) => {
|
||||||
|
// if everything sent ok
|
||||||
|
if (res.ok) {
|
||||||
|
// logout
|
||||||
|
localStorage.setItem("needsReload", "true");
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
class="h-full flex flex-col justify-center items-center"
|
||||||
|
v-if="loading"
|
||||||
|
>
|
||||||
|
<p class="text-3xl loading">
|
||||||
|
Loading<span>.</span><span>.</span><span>.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-col" v-for="post in posts">
|
||||||
|
<router-link
|
||||||
|
:to="`/post/${post._id}`"
|
||||||
|
class="flex flex-col items-center w-full h-fit border-b hover:bg-zinc-700 py-10"
|
||||||
|
>
|
||||||
|
<h1 class="text-2xl mb-4">
|
||||||
|
『 {{ post.title }} 』
|
||||||
|
</h1>
|
||||||
|
<p class="mb-4">
|
||||||
|
{{ post.author }} 🖊️ {{ post.date.substring(0, 10) }}
|
||||||
|
</p>
|
||||||
|
<p>{{ post.text }}</p>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center gap-10 mt-10"
|
||||||
|
v-if="hideForm == false"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
class="bg-zinc-900 border p-5"
|
||||||
|
v-model="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-10">
|
||||||
|
<label for="username">Password:</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="username"
|
||||||
|
class="bg-zinc-900 border p-5"
|
||||||
|
v-model="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="editUser(username, password)"
|
||||||
|
class="mt-2 hover:bg-emerald-700 py-5 w-[8rem] flex border justify-center"
|
||||||
|
>
|
||||||
|
🔄 Change
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="w-3/4 flex justify-center my-10 gap-10">
|
||||||
|
<button
|
||||||
|
v-if="hideForm"
|
||||||
|
@click="hideForm = false"
|
||||||
|
class="hover:bg-violet-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
Edit User
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="logout()"
|
||||||
|
class="hover:bg-red-700 py-5 w-[8rem] flex justify-center border"
|
||||||
|
>
|
||||||
|
🚪 Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
7
vite.config.js
Normal file
7
vite.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
})
|
||||||
Loading…
Add table
Reference in a new issue