Added libs

This commit is contained in:
Lucas
2026-01-25 13:55:46 +10:00
parent 575c682afc
commit f70af3c4ea
229 changed files with 26983 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
import { useP2postStore } from '../stores/p2postStore'
const p2postStore = useP2postStore()
const p2postApiUrl = "/api/p2post"
export const p2postApi = {
async getMyUsers(){
try {
const response = await fetch(p2postApiUrl+'/myusers')
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
}
const result = await response.json()
p2postStore.users = result
return result;
} catch (error) {
console.error("Error fetching posts:", error);
throw error;
}
},
async getPosts(){
try {
const response = await fetch(p2postApiUrl+'/posts')
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
}
const result = await response.json()
return result;
} catch (error) {
console.error("Error fetching posts:", error);
throw error;
}
},
async createPosts(user, content, medias, networks){
const data = {"user": user, "content": content, "medias": medias, "networks": networks};
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json',},
body: JSON.stringify(data),
};
try {
const response = await fetch(p2postApiUrl+"/posts", requestOptions);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error("Error fetching posts:", error);
throw error;
}
},
async uploadFile(file) {
const formData = new FormData();
formData.append("file", file);
try {
const response = await fetch(p2postApiUrl + "/medias", {
method: "POST",
body: formData,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error uploading file: ${response.status} - ${errorText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error("Error uploading file:", error);
throw error;
}
}
}

View File

@@ -0,0 +1,15 @@
import { useP2postStore } from '../stores/p2postStore'
const p2postStore = useP2postStore()
export default function registerSocketEvents(socket) {
socket.on('connect', (data) => {
console.log('Connected p2Post')
}),
socket.on('newPost', (data) => {
p2postStore.addPost(data)
console.log('New Post', data)
})
}

View File

@@ -0,0 +1,221 @@
<script setup>
import Card from '@/components/cards/Card.vue';
import { p2postApi } from '../api/p2postApi'
import { ref } from 'vue';
import InputComboBox from '@/components/inputs/InputComboBox.vue';
import Label from '@/components/labels/Label.vue';
import InputText from '@/components/inputs/InputText.vue';
import Button from '@/components/buttons/Button.vue';
import { ArrowUpTrayIcon, ChatBubbleBottomCenterTextIcon, RocketLaunchIcon, XMarkIcon } from '@heroicons/vue/24/solid';
const newPostUser = ref(null);
const newPostContent = ref("");
const newPostFiles = ref([]);
const newPostFilesPreview = ref([]);
const newPostNetworks = ref([]);
const onFileSelected = (e) => {
const newFiles = Array.from(e.target.files);
newFiles.forEach(file => {
const exists = newPostFiles.value.some(
f => f.name === file.name && f.size === file.size
);
if (!exists) {
newPostFiles.value.push(file);
newPostFilesPreview.value.push({
file,
url: URL.createObjectURL(file),
type: file.type
});
}
});
e.target.value = null;
};
function removePreview(index) {
const [removed] = newPostFilesPreview.value.splice(index, 1);
URL.revokeObjectURL(removed.url);
newPostFiles.value.splice(index, 1);
}
async function createPost(){
// TODO Select users
const user = "A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj"
const content = newPostContent.value
var medias = []
for (var file of newPostFilesPreview.value){
const response = await p2postApi.uploadFile(file.file)
console.log(response)
medias.push({type: "local",
"file_path": response.path})
}
// TODO Select networks
const networks = ["net"]
const response = await p2postApi.createPosts(user, content, medias, networks)
}
</script>
<template>
<Card class="bg-black border-yellow-400/20 p-6">
<h2 class="text-xl font-bold text-yellow-400 mb-6 flex items-center">
<ChatBubbleBottomCenterTextIcon class="mr-2 h-5 w-5" />
Create New Post
</h2>
<div class="space-y-4">
<div class="grid md:grid-cols-2 gap-4">
<div>
<Label class="text-gray-300 mb-2 block">Select User</Label>
<InputComboBox/>
</div>
<div class="bg-black border border-yellow-400/20 rounded-lg p-3">
<div class="flex items-center space-x-3">
<!-- <img src={user.photo || "/placeholder.svg"} alt="" class="w-10 h-10 rounded-full" /> -->
<div>
<p class="text-white font-semibold">{user.nickname}</p>
<p class="text-xs text-gray-400 font-mono">{user.publicKey}</p>
</div>
</div>
</div>
</div>
<div>
<Label class="text-gray-300 mb-2 block">Post Content</Label>
<InputText
value={postContent}
placeholder="Share your thoughts with the decentralized world..."
class="bg-black border-yellow-400/30 text-white min-h-24"
/>
</div>
<div class="flex gap-4">
<Button
@click=""
variant="outline"
class="border-yellow-400 text-yellow-400 hover:bg-yellow-400 hover:text-black bg-transparent"
>
<ArrowUpTrayIcon class="mr-2 h-4 w-4" />
Add Files
</Button>
<div class="flex-1 bg-black border border-yellow-400/20 rounded-lg p-3">
<div class="flex flex-wrap gap-2">
<div key={file} class="flex items-center space-x-2 bg-gray-800 rounded px-2 py-1">
<File size={14} class="text-yellow-400" />
<span class="text-white text-sm">{file}</span>
<Button
size="sm"
variant="ghost"
class="text-red-400 hover:bg-red-400/20 p-0 h-auto"
>
<XMarkIcon class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
<div>
<Label class="text-gray-300 mb-2 block">Select Networks</Label>
<!-- <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
<div key={network.id} class="flex items-center space-x-2">
<Checkbox
id={network.id}
checked={selectedNetworks.includes(network.name)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedNetworks((prev) => [...prev, network.name])
} else {
setSelectedNetworks((prev) => prev.filter((n) => n !== network.name))
}
class="border-yellow-400 data-[state=checked]:bg-yellow-400 data-[state=checked]:border-yellow-400"
/>
<Label htmlFor={network.id} class="text-white text-sm">
{network.name}
</Label>
</div>
))}
</div> -->
</div>
<Button
@click=""
class="bg-yellow-400 text-black hover:bg-yellow-300 font-bold"
>
<RocketLaunchIcon class="h-4 w-4 mr-2" />
Post to Networks
</Button>
</div>
</Card>
<!-- <div class="bg-gray-800 rounded-xl shadow p-4 mb-6 text-white">
<textarea
v-model="newPostContent"
placeholder="Share something with the network..."
class="w-full p-2 bg-gray-700 border border-gray-600 rounded mb-2"
rows="3"
></textarea>
<div
v-for="(preview, index) in newPostFilesPreview"
:key="preview.url"
class="bg-gray-700 p-2 rounded relative flex items-center justify-center h-40"
>
<button
@click="removePreview(index)"
class="absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-700"
title="Remover"
>
&times;
</button>
<img
v-if="preview.type.startsWith('image/')"
:src="preview.url"
class="object-contain max-h-full max-w-full"
alt="Preview"
/>
<audio
v-else-if="preview.type.startsWith('audio/')"
:src="preview.url"
controls
class="w-full"
></audio>
<video
v-else-if="preview.type.startsWith('video/')"
:src="preview.url"
controls
class="object-contain max-h-full max-w-full"
></video>
<div v-else class="text-sm text-center px-2 break-all">
{{ preview.file.name }}
</div>
</div>
<div class="flex items-center justify-between">
<input type="file" multiple @change="onFileSelected" class="text-sm text-white" />
<button
@click="createPost"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Post
</button>
</div>
</div> -->
</template>

View File

@@ -0,0 +1,79 @@
<script setup>
import Card from '@/components/cards/Card.vue';
import { useP2postStore } from '../stores/p2postStore';
import InputComboBox from '@/components/inputs/InputComboBox.vue';
import { ClockIcon, FunnelIcon, HashtagIcon } from '@heroicons/vue/24/solid';
const p2postStore = useP2postStore()
</script>
<template>
<Card class="bg-black border-yellow-400/20 p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-yellow-400 flex items-center">
<HashtagIcon class="mr-2 h-5 w-5" />
Feed
</h2>
<div class="flex items-center space-x-2">
<FunnelIcon class="text-gray-400 h-5 w-5" />
<InputComboBox></InputComboBox>
</div>
</div>
<div class="space-y-4">
<div key={post.id} class="bg-black border border-yellow-400/20 rounded-lg p-4">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center space-x-3">
<!-- <img src={post.photo || "/placeholder.svg"} alt="" class="w-12 h-12 rounded-full" /> -->
<div>
<div class="flex items-center space-x-2">
<span class="text-white font-semibold">{post.nickname}</span>
<div class="flex items-center space-x-1">
<Shield size={14} class="text-yellow-400" />
<span class="text-yellow-400 text-sm">PoW: {post.powDifficulty}</span>
</div>
</div>
<p class="text-xs text-gray-400 font-mono">{post.userPublicKey}</p>
</div>
</div>
<div class="flex items-center space-x-1 text-gray-400 text-sm">
<ClockIcon class="h-4 w-4"/>
<span>{new Date(post.postedAt).toLocaleString()}</span>
</div>
</div>
<div class="mb-3">
<p class="text-white leading-relaxed">{post.content}</p>
</div>
<div class="mb-3">
FILES
</div>
<div class="flex flex-wrap gap-2">
<span key={network} class="bg-yellow-400 text-black text-sm px-2 py-1 rounded-full">
{network}
</span>
</div>
</div>
</div>
</Card>
<!-- <div v-for="post in p2postStore.posts.value" :key="post.hash" class="bg-gray-800 p-4 rounded-xl shadow mb-4">
<div class="flex items-center space-x-3 mb-2">
<img :src="post.author.avatar" class="w-8 h-8 rounded-full" alt="Avatar" />
<div>
<p class="text-sm font-semibold">{{ post.author.nickname }}</p>
<p class="text-xs text-gray-400">{{ formatTimestamp(post.timestamp) }}</p>
</div>
</div>
<p class="text-gray-200 whitespace-pre-wrap">{{ post.content }}</p>
<div v-for="media in post.media">
<img v-if="media.type === 'external'" :src="media.url" class="mt-2 rounded max-h-60" alt="Media" />
<img v-if="media.type === 'local'" :src="media.file_path" class="mt-2 rounded max-h-60" alt="Media" />
</div>
</div> -->
</template>

View File

@@ -0,0 +1,219 @@
<script setup>
import Card from '@/components/cards/Card.vue';
import { useP2postStore } from '../stores/p2postStore';
import { ref } from 'vue';
import InputText from '@/components/inputs/InputText.vue';
import Button from '@/components/buttons/Button.vue';
import { CommandLineIcon, GlobeAltIcon, PencilIcon, PlusIcon, UsersIcon, XMarkIcon } from '@heroicons/vue/24/solid';
import { p2postApi } from '../api/p2postApi';
import { noSysApi } from '@/modules/noSys/api/noSysApi';
const p2postStore = useP2postStore()
const newNetworkName = ref("")
async function loadData(){
const networks = await noSysApi.listNetworks()
const users = await p2postApi.getMyUsers()
console.log(users)
for(const user of users){
const userData = await noSysApi.getUser(user.pubkey)
for(const network of userData.networks){
p2postStore.networks.push(networks.find(n => n.data.id === network))
}
}
}
loadData()
</script>
<template>
<div class="w-80 bg-black border-r border-yellow-400/20 p-6 space-y-6">
<Card class="bg-black border-yellow-400/20 p-4">
<h3 class="text-lg font-bold text-yellow-400 mb-4 flex items-center">
<UsersIcon class="h-5 w-5 mr-2" />
My Users
</h3>
<div class="space-y-3">
<div v-for="user in p2postStore.users" class="bg-black border border-yellow-400/20 rounded-lg p-3">
<div v-if="false" class="space-y-2">
<InputText
value={editNickname}
placeholder="Nickname"
class="bg-gray-800 border-yellow-400/30 text-white text-sm"
/>
<InputText
value={editBio}
placeholder="Bio"
class="bg-gray-800 border-yellow-400/30 text-white text-sm h-16"
/>
<div class="flex gap-2">
<Button
@click=""
class="bg-yellow-400 text-black hover:bg-yellow-300"
>
Save
</Button>
<Button
@click=""
class="border-gray-400 text-gray-400"
>
Cancel
</Button>
</div>
</div>
<div v-if="true">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<!-- <img src={user.photo || "/placeholder.svg"} alt="" class="w-8 h-8 rounded-full" /> -->
<span class="text-white font-semibold text-sm">{{user.nickname}}</span>
</div>
<Button
@click=""
class="text-black hover:bg-yellow-400/20"
>
<PencilIcon class="w-4 h-4" />
Edit
</Button>
</div>
<p class="text-xs text-gray-400 font-mono mb-1">{{user.pubkey}}</p>
<p class="text-xs text-gray-300">{{user.bio}}</p>
</div>
</div>
</div>
</Card>
<Card class="bg-black border-yellow-400/20 p-4">
<h3 class="text-lg font-bold text-yellow-400 mb-4 flex items-center">
<GlobeAltIcon class="h-5 w-5 mr-2" />
Networks
</h3>
<div class="space-y-3 mb-4">
<div v-for="network in p2postStore.networks" class="bg-black border border-yellow-400/20 rounded-lg p-3">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-white font-semibold">{{network.data.name}}</span>
<Button
size="sm"
variant="ghost"
@click=""
class="text-black hover:bg-yellow-400"
>
<XMarkIcon class="h-4 w-4" />
Remove
</Button>
</div>
<div class="text-xs text-gray-400 space-y-1">
<div>{{network.data.id}}</div>
<div>Posts: {{network.totalPosts}}</div>
</div>
</div>
</div>
<div class="flex gap-2">
<InputText
value={newNetworkName}
placeholder="Network name"
class="bg-black border-yellow-400/30 text-white text-sm"
/>
<Button size="sm" onClick={addNetwork} class="bg-yellow-400 text-black hover:bg-yellow-300">
<PlusIcon class="h-4 w-4"/>
Add
</Button>
</div>
</Card>
<Card v-if="false" class="bg-black border-yellow-400/20 p-4 flex-1">
<h3 class="text-lg font-bold text-yellow-400 mb-4 flex items-center">
<CommandLineIcon class="h-5 w-5 mr-2" />
System Logs
</h3>
<div
ref={logsRef}
class="bg-black border border-yellow-400/20 rounded-lg p-3 h-64 overflow-y-auto font-mono text-xs"
>
<!-- V-for logs -->
<div key={log.id} class="mb-1">
<span class="text-gray-500">{new Date(log.timestamp).toLocaleTimeString()}</span>
<span>
<!-- TODO log level/type colors -->
{log.message}
</span>
</div>
</div>
</Card>
</div>
<!-- <aside class="w-64 bg-gray-800 shadow-md p-4 overflow-y-auto">
<div class="mt-6">
<h3 class="text-lg font-semibold mb-2">Accounts</h3>
<div v-for="peer in p2postStore.peers.value" :key="peer.pubkey" class="flex items-center space-x-2 mb-2">
<img :src="peer.avatar" class="w-8 h-8 rounded-full" alt="Avatar" />
<span class="text-sm font-medium">{{ peer.nickname }}</span>
</div>
</div>
<div class="mt-6">
<h3 class="text-lg font-semibold mb-2">Networks</h3>
<ul>
<li
v-for="network in p2postStore.networks.value"
:key="network.name"
@click="selectNetwork(network)"
class="cursor-pointer hover:text-blue-400"
>
<span class="font-medium">{{ network.name }}</span>
<span class="text-sm text-gray-400 block">
{{ network.peers }} peers, {{ network.posts }} posts
</span>
<button @click="removeNetwork(network.name)" class="text-red-400 hover:underline">
Remove
</button>
</li>
</ul>
<form @submit.prevent="addNetwork" class="mb-4">
<input
v-model="newNetworkName"
type="text"
placeholder="New network..."
class="w-full bg-gray-700 border border-gray-600 rounded p-2 mb-2 text-sm text-white"
/>
<button
type="submit"
class="w-full bg-green-600 text-white text-sm py-1 rounded hover:bg-green-700"
>
Add Network
</button>
</form>
</div> -->
<!-- <div class="mt-6">
<h3 class="text-lg font-semibold mb-2">Accounts</h3>
<form @submit.prevent="updateUser" class="space-y-2">
<input
v-model="me.nickname"
type="text"
placeholder="Your nickname"
class="w-full bg-gray-700 border border-gray-600 rounded p-2 text-sm text-white"
/>
<input
type="file"
@change="onAvatarSelected"
class="text-sm text-white"
/>
<button
type="submit"
class="w-full bg-blue-600 text-white text-sm py-1 rounded hover:bg-blue-700"
>
Update Profile
</button>
</form>
<div class="mt-2 text-sm text-gray-400">
<p><strong>Public Key:</strong></p>
<p class="break-all text-xs text-gray-500">{{ me.pubkey }}</p>
</div>
</div> -->
<!-- </aside> -->
</template>

7
p2post/vue/router.js Normal file
View File

@@ -0,0 +1,7 @@
import HomeView from "./views/HomeView.vue";
const routes = [
{path: '/', name:'p2post', component: HomeView},
]
export {routes};

View File

@@ -0,0 +1,16 @@
import { defineStore } from 'pinia'
export const useP2postStore = defineStore('p2postStore', {
state: () => ({
users: [],
networks: [],
}),
getters: {
},
actions: {
}
})

View File

@@ -0,0 +1,57 @@
<script setup>
import { ref } from 'vue';
import { p2postApi } from '../api/p2postApi'
import CreatePost from '../components/CreatePost.vue'
import SideBar from '../components/SideBar.vue';
import Feed from '../components/Feed.vue'
import { useP2postStore } from '../stores/p2postStore';
const p2postStore = useP2postStore()
</script>
<template>
<div class="min-h-screen bg-black text-yellow-400">
<header class="border-b border-yellow-400/20 py-6">
<div class="container mx-auto px-6">
<div class="flex items-center space-x-4">
<div class="text-3xl font-black">
<span class="text-yellow-400">P2</span>
<span class="text-white">POST</span>
</div>
<div class="w-1 h-8 bg-yellow-400"></div>
<span class="text-gray-400">Decentralized Social Media</span>
</div>
</div>
</header>
<div class="flex min-h-screen">
<SideBar/>
<div class="flex-1 p-6 space-y-6">
<CreatePost/>
<Feed/>
</div>
</div>
</div>
<!-- <div class="flex h-screen bg-gray-900 text-white">
<SideBar/>
<main class="flex-1 p-6 overflow-y-auto">
<div class="max-w-2xl mx-auto">
<CreatePost/>
<Feed/>
</div>
</main>
</div> -->
</template>
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
background-color: #111827;
color: white;
}
</style>