+responsiv, +profile site,
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
<title>ESP - Express, Share, Post</title>
|
<title>ESP - Express, Share, Post</title>
|
||||||
<link rel="stylesheet" href="/src/assets/main.css" />
|
<link rel="stylesheet" href="/src/assets/main.css" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-hintergrund-farbe overflow-hidden">
|
<body class="bg-hintergrund-farbe sm:overflow-hidden">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Trending from "./home_components/trending.vue";
|
|||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="main" class="sm:flex overflow-y-auto h-screen scrollbar">
|
<div id="main" class="sm:flex overflow-y-auto sm:h-full sm:scrollbar">
|
||||||
<div id="left" class="sm:w-72 min-w-72">
|
<div id="left" class="sm:w-72 min-w-72">
|
||||||
<navigationbar></navigationbar>
|
<navigationbar></navigationbar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ async function createFeed() {
|
|||||||
const user_response = await fetch('http://localhost:8000/api/users', { method: 'GET' });
|
const user_response = await fetch('http://localhost:8000/api/users', { method: 'GET' });
|
||||||
const usersDATA = await user_response.json();
|
const usersDATA = await user_response.json();
|
||||||
|
|
||||||
|
if(post_response.status === 404 || user_response.status === 404) {
|
||||||
|
console.error("ERRROR");
|
||||||
|
await router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// posts und user kombinieren
|
// posts und user kombinieren
|
||||||
const combinedPosts = postsDATA.map(post => {
|
const combinedPosts = postsDATA.map(post => {
|
||||||
const user = usersDATA.find(user => user.user_id === post.user_id);
|
const user = usersDATA.find(user => user.user_id === post.user_id);
|
||||||
@@ -87,6 +93,10 @@ function copyLink(post_id: string | number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function post_create(post_text: string) {
|
async function post_create(post_text: string) {
|
||||||
|
if (post_text === "") {
|
||||||
|
alert("Please write something before posting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log(self_id);
|
console.log(self_id);
|
||||||
console.log(post_text);
|
console.log(post_text);
|
||||||
const response = await fetch(`http://localhost:8000/api/post/create`, {
|
const response = await fetch(`http://localhost:8000/api/post/create`, {
|
||||||
@@ -94,6 +104,7 @@ async function post_create(post_text: string) {
|
|||||||
headers: {'content-type': 'application/json'},
|
headers: {'content-type': 'application/json'},
|
||||||
body: `{"userId":${self_id},"postText":"${post_create_text}","postType":"text"}`});
|
body: `{"userId":${self_id},"postText":"${post_create_text}","postType":"text"}`});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log(response.text);
|
console.log(response.text);
|
||||||
await createFeed();
|
await createFeed();
|
||||||
@@ -102,6 +113,11 @@ async function post_create(post_text: string) {
|
|||||||
|
|
||||||
console.log(post_text);
|
console.log(post_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gotoProfile(user_id: string | number) {
|
||||||
|
router.push(`/profile/${user_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -121,7 +137,7 @@ async function post_create(post_text: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:overflow-y-auto sm:h-screen sm:scrollbar"> <!-- CONTENT -->
|
<div class="sm:overflow-y-auto sm:h-[650px] sm:scrollbar"> <!-- CONTENT -->
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(postitem, indexus) in upc" :key="upc" class="border-2 border-b-grau2 p-3 flex">
|
<li v-for="(postitem, indexus) in upc" :key="upc" class="border-2 border-b-grau2 p-3 flex">
|
||||||
<!-- POST -->
|
<!-- POST -->
|
||||||
@@ -134,21 +150,24 @@ async function post_create(post_text: string) {
|
|||||||
<div class="m-2"> <!-- POST CONTENT -->
|
<div class="m-2"> <!-- POST CONTENT -->
|
||||||
<p class="text-sm m-1 text-weiss">{{ postitem.post_text }}</p>
|
<p class="text-sm m-1 text-weiss">{{ postitem.post_text }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex"> <!-- POST FOOTER -->
|
<div class="sm:flex"><!-- POST FOOTER -->
|
||||||
<div class="flex"> <!-- Comments -->
|
<div class="flex">
|
||||||
<img src="../../assets/icons/comment.png" alt="" class="rounded-full align-middle">
|
<div class="flex"> <!-- Comments -->
|
||||||
<label class="text-sm m-1 text-weiss" v-if="postitem.comments != undefined">{{ postitem.comments }}</label>
|
<img src="../../assets/icons/comment.png" alt="" class="rounded-full align-middle">
|
||||||
<label class="text-sm m-1 text-weiss" v-else>Comments disabled</label>
|
<label class="text-sm m-1 text-weiss" v-if="postitem.comments != undefined">{{ postitem.comments }}</label>
|
||||||
</div>
|
<label class="text-sm m-1 text-weiss" v-else>Comments disabled</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center" @click="addLike(postitem.post_id, postitem.user_id, indexus)"> <!-- Likes -->
|
<div class="flex items-center" @click="addLike(postitem.post_id, postitem.user_id, indexus)"> <!-- Likes -->
|
||||||
<img alt="" src="../../assets/icons/herz.png" class="align-middle">
|
<img alt="" src="../../assets/icons/herz.png" class="align-middle">
|
||||||
<label class="text-sm m-1 text-weiss">{{ postitem.likes }}</label>
|
<label class="text-sm m-1 text-weiss">{{ postitem.likes }}</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<br class="sm:hidden">
|
||||||
<div class="flex items-center mx-2"> <!-- View Post -->
|
<div class="flex sm:tems-center mx-2"> <!-- View Post -->
|
||||||
<button @click="gotoPost(postitem.post_id)" class="text-schwarz mx-1 px-1 rounded-lg bg-button-farbe">View Post</button>
|
<button @click="gotoPost(postitem.post_id)" class="text-schwarz mx-1 px-1 rounded-lg bg-button-farbe">View Post</button>
|
||||||
<button @click="copyLink(postitem.post_id)" class="text-schwarz pl-1 mx-1 px-1 rounded-lg bg-button-farbe">Share Post</button>
|
<button @click="copyLink(postitem.post_id)" class="text-schwarz pl-1 mx-1 px-1 rounded-lg bg-button-farbe">Share Post</button>
|
||||||
|
<button @click="gotoProfile(postitem.user_id)" class="text-schwarz pl-1 mx-1 px-1 rounded-lg bg-button-farbe"> Go to Profile</button>
|
||||||
</div><!-- ENDE -->
|
</div><!-- ENDE -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function gotop() {
|
||||||
|
location.reload();
|
||||||
|
console.log("PARAMS: "+ route.path);
|
||||||
|
console.log("Zum Seitenanfang gescrollt");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-grau2 p-3">
|
<div class="text-grau2 p-3 text-center">
|
||||||
<a href="">Terms of Service</a>
|
<a href="">Terms of Service</a>
|
||||||
-
|
-
|
||||||
<a href="">Privacy Policy</a><br>
|
<a href="">Privacy Policy</a><br>
|
||||||
@@ -15,6 +24,9 @@
|
|||||||
<a href="https://esp-projekt.notion.site/191fb990f26c808298dad302e97fb299?pvs=105">Support</a>
|
<a href="https://esp-projekt.notion.site/191fb990f26c808298dad302e97fb299?pvs=105">Support</a>
|
||||||
-
|
-
|
||||||
<a href="https://icons8.com"> Icons by icons8.com</a>
|
<a href="https://icons8.com"> Icons by icons8.com</a>
|
||||||
|
|
||||||
|
<div class="sm:hidden flex justify-center pt-8"><button class="text-weiss p-1 m-2 rounded-lg py-3 px-5 bg-button-farbe transition duration-300 ease-in-out" @click="gotop">Back to Top</button></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import router from "../../router";
|
import router from "../../router";
|
||||||
import { ref, onMounted, onUnmounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
|
|
||||||
let self = localStorage.getItem("self_id");
|
let self;
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
const show = ref(false);
|
const show = ref(false);
|
||||||
|
|
||||||
@@ -45,6 +45,8 @@ onMounted(() => {
|
|||||||
if(localStorage.getItem("mobile") === null){
|
if(localStorage.getItem("mobile") === null){
|
||||||
show.value = false;
|
show.value = false;
|
||||||
}
|
}
|
||||||
|
self = localStorage.getItem("self_id");
|
||||||
|
console.log("SELF NB: " + self);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ async function login(event: Event) {
|
|||||||
const username = input_username_mail;
|
const username = input_username_mail;
|
||||||
const password = input_user_password;
|
const password = input_user_password;
|
||||||
|
|
||||||
|
if (username.value === "" || password.value === "") {
|
||||||
|
alert("Please fill all fields");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:8000/api/account/login', {
|
const response = await fetch('http://localhost:8000/api/account/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -34,6 +39,7 @@ async function login(event: Event) {
|
|||||||
localStorage.setItem('isLoggedIn', 'true');
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
localStorage.setItem('username', username.value);
|
localStorage.setItem('username', username.value);
|
||||||
localStorage.setItem('self_id', data["userId"]);
|
localStorage.setItem('self_id', data["userId"]);
|
||||||
|
console.log("SELF LOG: " + data["userId"]);
|
||||||
alert("You will be now redirected");
|
alert("You will be now redirected");
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ import Msg_main from "./messages_components/msg_main.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="main" class=" flex">
|
<div id="main" class=" sm:flex">
|
||||||
<div id="left" class="w-72">
|
<div id="left" class="sm:w-72 min-w-72">
|
||||||
<navigationbar></navigationbar>
|
<navigationbar></navigationbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100p">
|
<div class="w-100p">
|
||||||
<msg_main></msg_main>
|
<msg_main></msg_main>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4">
|
<div class="sm:w-1/4">
|
||||||
<contacts></contacts>
|
<contacts></contacts>
|
||||||
<legal></legal>
|
<legal></legal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ function openChat(contact) {
|
|||||||
<div class="flex p-4">
|
<div class="flex p-4">
|
||||||
<img src="../../assets/default_pp.png" alt="user profile picture" class="rounded-full w-16 h-16">
|
<img src="../../assets/default_pp.png" alt="user profile picture" class="rounded-full w-16 h-16">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex">
|
<div class="flex mb-1">
|
||||||
<label class="text-lg font-bold m-1 text-weiss">{{nachricht.display_name}}</label>
|
<label class="text-lg font-bold sm:m-1 ml-1 text-weiss">{{nachricht.display_name}}</label>
|
||||||
<label class="text-lg m-1 text-grau2">@{{nachricht.username}}</label>
|
<label class="text-lg sm:m-1 ml-1 text-grau2">@{{nachricht.username}}</label>
|
||||||
<label class="m-2 text-grau2">{{nachricht.date}}</label>
|
<label class="sm:m-2 ml-1 text-grau2">{{nachricht.date}}</label>
|
||||||
</div>
|
</div>
|
||||||
<a class="ml-1 text-weiss">{{nachricht.content}}</a>
|
<a class="ml-1 text-weiss">{{nachricht.content}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import Contacts from "./home_components/contacts.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-weiss flex">
|
<div class="text-weiss sm:flex">
|
||||||
<div id="left" class="border-1 border-b-grau w-72">
|
<div id="left" class="border-1 border-b-grau sm:w-72 min-w-72">
|
||||||
<navigationbar></navigationbar>
|
<navigationbar></navigationbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100p border-x-2 border-x-grau2">
|
<div class="w-100p border-x-2 border-x-grau2">
|
||||||
<notifi_comp></notifi_comp>
|
<notifi_comp></notifi_comp>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4">
|
<div class="sm:w-1/4">
|
||||||
<contacts></contacts>
|
<contacts></contacts>
|
||||||
<legal></legal>
|
<legal></legal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ async function getPost(post_id: any) {
|
|||||||
|
|
||||||
if(post_response.status === 404) {
|
if(post_response.status === 404) {
|
||||||
console.error("No comments found.");
|
console.error("No comments found.");
|
||||||
|
alert("Post not found")
|
||||||
await router.push('/');
|
await router.push('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -102,6 +103,13 @@ async function getComment(post_id: any) {
|
|||||||
const user_response = await fetch(`http://localhost:8000/api/users`, { method: 'GET' });
|
const user_response = await fetch(`http://localhost:8000/api/users`, { method: 'GET' });
|
||||||
const usersDATA: User[] = await user_response.json();
|
const usersDATA: User[] = await user_response.json();
|
||||||
|
|
||||||
|
if(comments_response.status === 404 || user_response.status === 404) {
|
||||||
|
console.error("ERRROR");
|
||||||
|
alert("Error try it again later.");
|
||||||
|
await router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
comments.value = fetchedComments.map(comment => {
|
comments.value = fetchedComments.map(comment => {
|
||||||
const author = usersDATA.find(u => u.user_id === comment.author_user_id) || {
|
const author = usersDATA.find(u => u.user_id === comment.author_user_id) || {
|
||||||
username: 'Unknown',
|
username: 'Unknown',
|
||||||
@@ -115,6 +123,8 @@ async function getComment(post_id: any) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
comments.value.sort((a, b) => b.post_id - a.post_id);
|
||||||
|
|
||||||
console.log(comments.value);
|
console.log(comments.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -136,11 +146,10 @@ async function addLike() { // Post liken
|
|||||||
body: `{"userId":${self_id}}`,
|
body: `{"userId":${self_id}}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response_like.ok) {
|
if(response_like.status === 404) {
|
||||||
const errorText = await response_like.text();
|
console.error("ERROR");
|
||||||
console.error('Server-Fehlertext:', errorText);
|
await router.push('/');
|
||||||
//post.value.likes--;
|
return;
|
||||||
throw new Error(`HTTP error! status: ${response_like.status}, text: ${errorText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response_like.json();
|
const data = await response_like.json();
|
||||||
@@ -154,6 +163,10 @@ async function addLike() { // Post liken
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function comment_create_text(comment_text: string) {
|
async function comment_create_text(comment_text: string) {
|
||||||
|
if (comment_text === null) {
|
||||||
|
alert("Please write something before commenting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8000/api/post/${post_uuid.value}/comment`, {
|
const response = await fetch(`http://localhost:8000/api/post/${post_uuid.value}/comment`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -162,11 +175,13 @@ async function comment_create_text(comment_text: string) {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if(response.ok) {
|
if(response.status === 404) {
|
||||||
await getComment(parseInt(post_uuid.value));
|
console.error("ERROR");
|
||||||
} else {
|
await router.push('/');
|
||||||
alert("Error while posting comment.");
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await getComment(parseInt(post_uuid.value));
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -183,11 +198,10 @@ async function addLike_comment(comment_id: number | string) {
|
|||||||
body: `{"userId":${self_id}}`,
|
body: `{"userId":${self_id}}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response_like.ok) {
|
if(response_like.status === 404) {
|
||||||
const errorText = await response_like.text();
|
console.error("ERROR");
|
||||||
console.error('Server-Fehlertext:', errorText);
|
await router.push('/');
|
||||||
post.value.likes--;
|
return;
|
||||||
throw new Error(`HTTP error! status: ${response_like.status}, text: ${errorText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response_like.json();
|
const data = await response_like.json();
|
||||||
@@ -201,11 +215,15 @@ async function addLike_comment(comment_id: number | string) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gotoProfile(user_id: string | number) {
|
||||||
|
router.push(`/profile/${user_id}`);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="main" class="bg-hintergrund-farbe flex">
|
<div id="main" class="bg-hintergrund-farbe flex">
|
||||||
<div id="left" class="w-72">
|
<div id="left" class="sm:w-72 min-w-72">
|
||||||
<navigationbar></navigationbar>
|
<navigationbar></navigationbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-weiss w-100p border-x border-x-grau2" v-if="post">
|
<div class="text-weiss w-100p border-x border-x-grau2" v-if="post">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Profile_main from "./profile_components/profile_main.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="main" class="bg-hintergrund-farbe sm:flex overflow-y-auto h-screen scrollbar">
|
<div id="main" class="bg-hintergrund-farbe sm:flex overflow-visible scrollbar sm:overflow-y-auto sm:h-full sm:scrollbar">
|
||||||
<div id="left" class="sm:w-72 min-w-72">
|
<div id="left" class="sm:w-72 min-w-72">
|
||||||
<navigationbar></navigationbar>
|
<navigationbar></navigationbar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,90 +6,64 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const upc = ref([]);
|
const upc = ref([]);
|
||||||
let self_id ;
|
const profile_id = ref<number | null>(null);
|
||||||
let profile_id = ref();
|
const userData = ref<any>(null);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log("PARAMS: "+ route.path);
|
|
||||||
const pathArray = route.path.split('/');
|
const pathArray = route.path.split('/');
|
||||||
console.log(pathArray);
|
|
||||||
|
|
||||||
if (pathArray.length > 2) {
|
if (pathArray.length > 2) {
|
||||||
profile_id.value = pathArray[2];
|
profile_id.value = parseInt(pathArray[2], 10);
|
||||||
console.log("profile_id 0: ", profile_id.value);
|
} else {
|
||||||
|
console.warn("No profile ID found in the route.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profile_id) {
|
if (!profile_id.value) {
|
||||||
alert('No profile selected. Redirecting to feed.');
|
alert('No profile selected. Redirecting to feed.');
|
||||||
await router.push('/');
|
await router.push('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await create_own_posts();
|
await create_own_posts();
|
||||||
|
await getUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function create_own_posts() {
|
async function create_own_posts() {
|
||||||
try {
|
try {
|
||||||
// posts und user holen und schauen ob sie richtig sidn
|
const post_response = await fetch('http://localhost:8000/api/posts', {
|
||||||
const post_response = await fetch('http://localhost:8000/api/posts', { method: 'GET' });
|
method: 'GET',
|
||||||
|
});
|
||||||
if (!post_response.ok) {
|
if (!post_response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${post_response.status}`);
|
throw new Error(`HTTP error! status: ${post_response.status}`);
|
||||||
}
|
}
|
||||||
const postsDATA = await post_response.json();
|
const postsDATA = await post_response.json();
|
||||||
|
|
||||||
const user_response = await fetch('http://localhost:8000/api/users', { method: 'GET' });
|
upc.value = postsDATA.filter((post) => post.user_id === profile_id.value);
|
||||||
if (!user_response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${user_response.status}`);
|
if (upc.value.length === 0) {
|
||||||
|
console.warn('No posts found for this user.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const usersDATA = await user_response.json();
|
//console.log("upc: "+ JSON.stringify(upc.value, null, 2));
|
||||||
|
|
||||||
// posts und user kombinieren
|
return upc.value;
|
||||||
const combinedPosts = postsDATA.filter(post => post.user_id === profile_id).map(post => {
|
} catch (error) {
|
||||||
const user = usersDATA.find(user => user.user_id === post.user_id);
|
console.error('Error fetching posts:', error);
|
||||||
|
upc.value = [];
|
||||||
return {
|
|
||||||
post_id: post.posts_uuid,
|
|
||||||
post_text: post.post_text,
|
|
||||||
likes: post.likes,
|
|
||||||
comments: post.comments,
|
|
||||||
displayname: user ? user.displayname : 'Unknown',
|
|
||||||
username: user ? user.username : 'unknown_user',
|
|
||||||
user_id: post.user_id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
console.log("upc: " + upc.value);
|
|
||||||
console.log("combinedPosts: " + combinedPosts);
|
|
||||||
|
|
||||||
//upc.value = combinedPosts;
|
|
||||||
|
|
||||||
upc.value = combinedPosts.sort((a, b) => b.post_id - a.post_id);;
|
|
||||||
|
|
||||||
console.log("upc 2: " + upc.value);
|
|
||||||
console.log("combinedPosts 2: " + combinedPosts);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("An error has occurred. Please try again later.");
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
console.log(upc.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addLike(post_id: string | number, user_id: number, index: number) {
|
async function addLike(post_id: string | number, user_id: number, index: number) {
|
||||||
try {
|
try {
|
||||||
console.log("UPC: ", upc.value);
|
|
||||||
console.log("post_id: ", post_id);
|
|
||||||
upc.value[index].likes++;
|
upc.value[index].likes++;
|
||||||
const response = await fetch(`http://localhost:8000/api/post/${post_id}/like`, {
|
const response = await fetch(`http://localhost:8000/api/post/${post_id}/like`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'content-type': 'application/json'},
|
headers: { 'content-type': 'application/json' },
|
||||||
body: `{"userId":${user_id}}`,
|
body: `{"userId":${user_id}}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Antwort-Status:', response.status);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('Server-Fehlertext:', errorText);
|
console.error('Server-Fehlertext:', errorText);
|
||||||
//upc.value[index].likes--;
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}, text: ${errorText}`);
|
throw new Error(`HTTP error! status: ${response.status}, text: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,68 +76,124 @@ async function addLike(post_id: string | number, user_id: number, index: number)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function consoleLog() {
|
||||||
|
console.log("upc: ", upc.value);
|
||||||
|
console.log("profile_id: ", profile_id.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoPost(post_id: string | number) {
|
||||||
|
localStorage.setItem("viewedpost", post_id.toString());
|
||||||
|
router.push(`/post/${post_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyLink(post_id: string | number) {
|
||||||
|
const tocopy = `http://localhost:5173/post/${post_id}`;
|
||||||
|
navigator.clipboard.writeText(tocopy);
|
||||||
|
alert("Copied to clipboard with");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUser() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:8000/api/users/');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const users = await response.json();
|
||||||
|
const user = users.find((u) => u.user_id === profile_id.value);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const followerCount = JSON.parse(user.followers).length;
|
||||||
|
const followingCount = JSON.parse(user.following).length;
|
||||||
|
|
||||||
|
userData.value = {
|
||||||
|
...user,
|
||||||
|
followerCount,
|
||||||
|
followingCount,
|
||||||
|
};
|
||||||
|
console.log("userData: ", userData.value);
|
||||||
|
return userData;
|
||||||
|
} else {
|
||||||
|
console.error('Benutzer nicht gefunden.');
|
||||||
|
userData.value = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Abrufen der Benutzerdaten:', error);
|
||||||
|
userData.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyUser(){
|
||||||
|
const tocopy = `http://localhost:5173/profile/${profile_id.value}`;
|
||||||
|
navigator.clipboard.writeText(tocopy);
|
||||||
|
alert("Copied to clipboard");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div><!-- MAIN -->
|
|
||||||
<div> <!-- FEED HEADER -->
|
|
||||||
<h2 class="align-middle p-6 text-3xl text-weiss border-b-grau2 border-b ">Profile</h2>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div class="text-weiss p-4"> <!-- PROFILE-->
|
<h2 class="align-middle p-6 text-3xl text-weiss border-b-grau2 border-b ">Profile</h2>
|
||||||
<div class="flex justify-center">
|
<div class="mb-12" v-if="userData">
|
||||||
<img src="../../assets/danielvici_pp.png" alt="" class="size-48 rounded-full"/>
|
<div class="text-weiss p-4 flex justify-center">
|
||||||
|
<img src="../../assets/default_pp.png" alt="" class="size-36 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center p-5 flex flex-col">
|
<div class="text-center p-5 flex flex-col">
|
||||||
<label class="text-xl font-bold m-1 text-weiss">danielvici123</label>
|
<label class="text-xl font-bold m-1 text-weiss" @click="consoleLog()">{{ userData.displayname }}</label>
|
||||||
<label class="text-base m-1 text-grau2">@danielvici</label>
|
<label class="text-base m-1 text-grau2">@{{ userData.username }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-grau2 p2 text-center">
|
<div class="text-grau2 p2 text-center">
|
||||||
<label class="text-base m-1 p-2"> Followers <a class="text-weiss">151</a></label>
|
<label class="text-base m-1 p-2"> Followers <a class="text-weiss">{{ userData.followerCount }}</a></label>
|
||||||
<label class="text-base m-1 p-2"> Following <a class="text-weiss">2625</a></label>
|
<label class="text-base m-1 p-2"> Following <a class="text-weiss">{{ userData.followingCount }}</a></label>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center pt-5">
|
||||||
|
<button @click="copyUser" class="text-schwarz mx-1 px-1 rounded-lg bg-button-farbe">Share Profile</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else class="flex-col justify-center p-5 text-center">
|
||||||
|
<a class="text-weiss text-3xl"> USER NOT FOUND</a> <br>
|
||||||
<div> <!-- POSTS -->
|
<router-link to="/" class="text-button-farbe text-3xl text-center"> Go to Feed</router-link>
|
||||||
<div>
|
|
||||||
<h2 class="align-middle mt-4 p-6 text-2xl text-weiss border-y-grau2 border-y ">Posts (x)</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<div>
|
||||||
<li v-for="(postitem, indexus) in upc" :key="postitem.user_id" class="border-2 border-b-grau2 p-3 flex">
|
<h2 class="align-middle p-6 text-xl text-weiss border-y-grau2 border-y ">Posts</h2>
|
||||||
<!-- POST -->
|
</div>
|
||||||
<img src="../../assets/danielvici_pp.png" alt="" class="rounded-full w-16 h-16">
|
<div class="sm:overflow-y-auto sm:h-[350px] sm:scrollbar">
|
||||||
<div>
|
<ul v-if="upc.length > 0">
|
||||||
<div> <!-- POST HEADER -->
|
<li v-for="(postitem, indexus) in upc" :key="postitem.user_id" class="border border-grau2 p-3 flex">
|
||||||
<label class="text-lg font-bold m-1 text-weiss">{{postitem.author_display_name}}</label>
|
<img src="../../assets/default_pp.png" alt="" class="rounded-full w-16 h-16">
|
||||||
<label class="text-base m-1 text-grau2">@{{ postitem.author_username }}</label>
|
<div>
|
||||||
</div>
|
<div>
|
||||||
<div class="m-2"> <!-- POST CONTENT -->
|
<label class="text-lg font-bold m-1 text-weiss">{{ userData.displayname }}</label>
|
||||||
<p class="text-sm m-1 text-weiss">{{ postitem.content }}</p>
|
<label class="text-base m-1 text-grau2">@{{ userData.username }}</label>
|
||||||
</div>
|
|
||||||
<div class="flex"> <!-- POST FOOTER -->
|
|
||||||
<div class="flex"> <!-- Comments -->
|
|
||||||
<img src="../../assets/icons/comment.png" alt="" class="rounded-full align-middle">
|
|
||||||
<label class="text-sm m-1 text-weiss" v-if="postitem.comments_count != undefined">{{ postitem.comments_count }}</label>
|
|
||||||
<label class="text-sm m-1 text-weiss" v-else>Comments disabled</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="m-2">
|
||||||
<div class="flex items-center" @click="addLike(postitem.post_id, postitem.user_id, indexus)"> <!-- Likes -->
|
<p class="text-sm m-1 text-weiss">{{ postitem.post_text }}</p>
|
||||||
<img alt="" src="../../assets/icons/herz.png" class="align-middle">
|
|
||||||
<label class="text-sm m-1 text-weiss">{{ postitem.likes }}</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sm:flex">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex">
|
||||||
|
<img src="../../assets/icons/comment.png" alt="" class="rounded-full align-middle">
|
||||||
|
<label class="text-sm m-1 text-weiss" v-if="postitem.comments != undefined">{{ postitem.comments }}</label>
|
||||||
|
<label class="text-sm m-1 text-weiss" v-else>Comments disabled or no comments</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mx-2"> <!-- View Post -->
|
<div class="flex items-center" @click="addLike(postitem.post_id, postitem.user_id, indexus)">
|
||||||
<router-link :to="{ name: 'PostDetail', params: { id: postitem.id } }" class="text-weiss">View Post</router-link>
|
<img alt="" src="../../assets/icons/herz.png" class="align-middle">
|
||||||
</div><!-- ENDE -->
|
<label class="text-sm m-1 text-weiss">{{ postitem.likes }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br class="sm:hidden">
|
||||||
|
<div class="flex sm:items-center mx-2">
|
||||||
|
<button @click="gotoPost(postitem.posts_uuid)" class="text-schwarz mx-1 px-1 rounded-lg bg-button-farbe">View Post</button>
|
||||||
|
<button @click="copyLink(postitem.posts_uuid)" class="text-schwarz pl-1 mx-1 px-1 rounded-lg bg-button-farbe">Share Post</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
<p v-else class="text-weiss text-center justify-center text-lg pt-5"> This user has not posted anything yet. </p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -16,6 +16,11 @@ async function register() {
|
|||||||
const password = register_input_password;
|
const password = register_input_password;
|
||||||
const std_text = "default";
|
const std_text = "default";
|
||||||
|
|
||||||
|
if (username.value === "" || password.value === "" || displayname.value === "" || email.value === "") {
|
||||||
|
alert("Please fill all fields");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
console.log("Username: " + username.value + ", Password: " + password.value);
|
console.log("Username: " + username.value + ", Password: " + password.value);
|
||||||
|
|
||||||
@@ -34,17 +39,17 @@ async function register() {
|
|||||||
localStorage.setItem('isLoggedIn', 'true');
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
localStorage.setItem('username', username.value);
|
localStorage.setItem('username', username.value);
|
||||||
localStorage.setItem('self_id', data["userId"]);
|
localStorage.setItem('self_id', data["userId"]);
|
||||||
|
console.log("SELF REG: " + data["userId"]);
|
||||||
alert("Account created! You will be now redirected");
|
alert("Account created! You will be now redirected");
|
||||||
router.push('/');
|
router.push('/');
|
||||||
router.go(1);
|
router.go(1);
|
||||||
} else {
|
|
||||||
alert("Something went wrong. Please try again later.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log(response);
|
console.log(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("An error has occurred. Please try again later.");
|
console.log("An error has occurred. Please try again later.");
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import Contacts from "./home_components/contacts.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-weiss flex">
|
<div class="text-weiss sm:flex">
|
||||||
<div id="left" class="sm:w-72 min-w-72 border-1 border-b-grau">
|
<div id="left" class="sm:w-72 sm:min-w-72 border-1 border-b-grau">
|
||||||
<navigationbar></navigationbar>
|
<navigationbar></navigationbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:w-100p w-screen border-x-2 border-x-grau2">
|
<div class="sm:w-100p w-screen border-x-2 border-x-grau2">
|
||||||
@@ -18,7 +18,7 @@ import Contacts from "./home_components/contacts.vue";
|
|||||||
</div>
|
</div>
|
||||||
<search_main></search_main>
|
<search_main></search_main>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4">
|
<div class="sm:w-1/4">
|
||||||
<contacts></contacts>
|
<contacts></contacts>
|
||||||
<legal></legal>
|
<legal></legal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,30 +119,18 @@ function go_fs(){
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex justify-center"> <!-- Search-->
|
<div class="flex justify-center"> <!-- Search-->
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<form @submit.prevent="go_fs">
|
<form @submit.prevent="go_fs" class="flex flex-col">
|
||||||
<input type="text" placeholder="Search..." class="w-full m-2 mt-6 p-4 bg-grau-dunkel focus:outline-none rounded-xl placeholder:text-center text-center" v-model="usr_search">
|
<input type="text" placeholder="Search..." class="w-full m-2 mt-6 p-4 bg-grau-dunkel focus:outline-none rounded-xl placeholder:text-center text-center" v-model="usr_search">
|
||||||
<div class="flex justify-center text-grau2">
|
<div class="flex justify-center text-grau2">
|
||||||
<label class="m-2 accent-logo-farbe-blau"><input type="checkbox" class="mr-1" v-model="search_filter_status.u">User</label>
|
<label class="m-2 accent-logo-farbe-blau"><input type="checkbox" class="mr-1" v-model="search_filter_status.u">User</label>
|
||||||
<label class="m-2 accent-logo-farbe-rot" ><input type="checkbox" class="mr-1" v-model="search_filter_status.h">Hashtag</label>
|
<label class="m-2 accent-logo-farbe-rot" ><input type="checkbox" class="mr-1" v-model="search_filter_status.h">Hashtag</label>
|
||||||
<label class="m-2 accent-logo-farbe-lila"><input type="checkbox" class="mr-1" v-model="search_filter_status.p">Post</label>
|
<label class="m-2 accent-logo-farbe-lila"><input type="checkbox" class="mr-1" v-model="search_filter_status.p">Post</label>
|
||||||
<button class="px-1 text-weiss rounded-lg">Filter</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="text-schwarz pl-1 mx-1 px-1 rounded-lg bg-button-farbe w-1/2 place-self-center">Filter</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div> <!-- TRENDING HASHTAGS -->
|
|
||||||
<a class="text-2xl flex justify-center mt-4">Trending</a>
|
|
||||||
<ul class="flex justify-center ">
|
|
||||||
<li v-for="(bing, i) in most_posts_hashtags" :key="bing.id" class="w-2/12">
|
|
||||||
<div class="p-5 mt-4 border-b-grau2 border-b">
|
|
||||||
<p class="text-sm m-1 text-grau2">{{ i+1 }} - {{ bing.category}}</p>
|
|
||||||
<h1 class="text-xl font-bold m-1 text-weiss">#{{ bing.name }}</h1>
|
|
||||||
<p class="text-sm m-1 text-grau2">{{ bing.nr_posts }} posts</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<!-- ### ### ### ### ### ### ### -->
|
<!-- ### ### ### ### ### ### ### -->
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
@@ -150,7 +138,7 @@ function go_fs(){
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="feed.length > 0">
|
<div v-if="feed.length > 0">
|
||||||
<div v-for="(bing, i) in feed" :key="bing" class=""> <!-- SEARCH RESULTS -->
|
<div v-for="(bing, i) in feed" :key="bing" class=""> <!-- SEARCH RESULTS -->
|
||||||
<div v-if="bing.type === 'user'" class="pt-2 p-3 border-b-grau2 border-b" @click="router.push('/profile/${bing.username}')"> <!-- --- USER RESULT --- -->
|
<div v-if="bing.type === 'user'" class="pt-2 p-3 border-b-grau2 border-b"> <!-- --- USER RESULT --- -->
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<img src="/src/assets/default_pp.png" alt="profile picture" class="rounded-full w-16 h-16">
|
<img src="/src/assets/default_pp.png" alt="profile picture" class="rounded-full w-16 h-16">
|
||||||
<div class="">
|
<div class="">
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ function handleUpdateSetting(setting: string) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="main" class="bg-hintergrund-farbe flex">
|
<div id="main" class="bg-hintergrund-farbe sm:flex">
|
||||||
<div id="left" class=" w-72">
|
<div id="left" class="sm:w-72 min-w-72">
|
||||||
<navigationbar></navigationbar>
|
<navigationbar></navigationbar>
|
||||||
<settings_sidebar @updateSetting="handleUpdateSetting"></settings_sidebar>
|
<settings_sidebar @updateSetting="handleUpdateSetting"></settings_sidebar>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100p border-x-grau2 border-x-2">
|
<div class="w-100p sm:border-x-grau2 sm:border-x-2 border-y border-y-grau2 my-2">
|
||||||
<settings_main :selectedSetting="selectedSetting"></settings_main>
|
<settings_main :selectedSetting="selectedSetting"></settings_main>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4">
|
<div class="sm:w-1/4">
|
||||||
<contacts></contacts>
|
<contacts></contacts>
|
||||||
<legal></legal>
|
<legal></legal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ console.log(`Setting got (SM): ${props.selectedSetting}`);
|
|||||||
|
|
||||||
|
|
||||||
<div v-else-if="props.selectedSetting === 'setting_messages'"> <!-- MESSAGES-->
|
<div v-else-if="props.selectedSetting === 'setting_messages'"> <!-- MESSAGES-->
|
||||||
<label>MOIN2</label>
|
<label class="text-center">Hey! So you found this setting? </label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="props.selectedSetting === 'setting_other'"> <!-- OTHER -->
|
<div v-else-if="props.selectedSetting === 'setting_other'"> <!-- OTHER -->
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function logout() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-grau2 font-bold space-y-4 pl-2 pt-5">
|
<div class="text-grau2 font-bold space-y-4 pl-2 pt-5 mb-4">
|
||||||
<label class="flex text-center shadow-2xl rounded-lg active:text-weiss" @click="logout"><img class="pr-2" src="../../assets/icons/logout.png" alt="">Logout</label>
|
<label class="flex text-center shadow-2xl rounded-lg active:text-weiss" @click="logout"><img class="pr-2" src="../../assets/icons/logout.png" alt="">Logout</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -80,10 +80,9 @@ const router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (to.meta.requiresAuth && localStorage.getItem("isLoggedIn") !== "true") {
|
if (to.meta.requiresAuth && localStorage.getItem("isLoggedIn") !== "true") {
|
||||||
console.log("User not logged in: redirecting to login.");
|
alert("not logged in ");
|
||||||
next({ name: "login" });
|
next({ name: "login" });
|
||||||
} else {
|
} else {
|
||||||
console.log("User logged in or no auth required.");
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user