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