functional version

This commit is contained in:
ak 2023-09-30 22:01:15 -07:00
parent 00288613fc
commit b2d56b7dfd
20 changed files with 2834 additions and 0 deletions

25
.gitignore vendored Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

24
package.json Normal file
View 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
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

21
src/App.vue Normal file
View 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
View 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 };

View 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">&#129399; {{ username }}</h1>
</div>
</router-link>
</nav>
<div class="h-full flex justify-center">
<router-view @title="(title) => changeTitle(title)"></router-view>
</div>
</template>

View 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
View 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
View 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
View 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
View 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
View 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">
&#x300E;&nbsp;&nbsp;&nbsp;{{ post.title }}&nbsp;&nbsp;&nbsp;&#x300F;
</h1>
<p class="mb-4">
{{ post.author }} &#128394;&#65039; {{ 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"
>
&#x1F680; Publish
</button>
<button
@click="savePost(postTitle, postText)"
class="hover:bg-yellow-700 py-5 w-[8rem] flex justify-center border"
>
&#x1F4BE; Save
</button>
</div>
</div>
</div>
</div>
</template>

59
src/views/Login.vue Normal file
View 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"
>
&#128273; Log In
</button>
</div>
</template>

238
src/views/Post.vue Normal file
View 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 }} &#128394;&#65039; {{ 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"
>&#128394;&#65039; 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"
>
&#128686; 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 }} &#128394;&#65039;
{{ 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:&nbsp</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"
>
&#9989; Submit
</button>
</div>
</div>
</div>
</div>
</div>
</template>

64
src/views/Signup.vue Normal file
View 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"
>
&#128587; Sign Up
</button>
</div>
</template>

167
src/views/User.vue Normal file
View 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">
&#x300E;&nbsp;&nbsp;&nbsp;{{ post.title }}&nbsp;&nbsp;&nbsp;&#x300F;
</h1>
<p class="mb-4">
{{ post.author }} &#128394;&#65039; {{ 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"
>
&#x1F504; 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"
>
&#x1F6AA; Logout
</button>
</div>
</div>
</template>

8
tailwind.config.js Normal file
View 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
View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})