mirror of
https://github.com/BradNut/svelteKitForBeginners
synced 2025-09-08 17:40:24 +00:00
Fetch tweets in an api and design how it shows up in the feed.
This commit is contained in:
parent
cf52758c94
commit
6aebae4688
8 changed files with 351 additions and 1 deletions
219
src/components/tweet.svelte
Normal file
219
src/components/tweet.svelte
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition'
|
||||
|
||||
import Icon from '$root/components/icon.svelte'
|
||||
import type { TweetType } from '$root/types'
|
||||
|
||||
export let tweet: TweetType
|
||||
</script>
|
||||
|
||||
<article class="tweet-container" transition:fade>
|
||||
<a class="avatar" href="/home/profile/{tweet.name}">
|
||||
<img
|
||||
width="140"
|
||||
height="140"
|
||||
src={tweet.avatar}
|
||||
alt={tweet.name}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div class="tweet-details">
|
||||
<div>
|
||||
<a href="/home/profile/{tweet.name}" class="user">
|
||||
{tweet.name}
|
||||
</a>
|
||||
<span class="handle">{tweet.handle}</span>
|
||||
<span class="posted"> · {tweet.posted}</span>
|
||||
</div>
|
||||
|
||||
<div class="tweet">
|
||||
<div class="content">
|
||||
{tweet.content}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<form action="/home/like" method="post">
|
||||
<input type="hidden" name="id" value={tweet.id} />
|
||||
<button
|
||||
class="btn like"
|
||||
title="Like"
|
||||
type="submit"
|
||||
>
|
||||
<div class="circle">
|
||||
<Icon
|
||||
width="24"
|
||||
height="24"
|
||||
name="like"
|
||||
class={tweet.liked ? 'liked' : ''}
|
||||
/>
|
||||
</div>
|
||||
<span class="count">
|
||||
{#key tweet.likes}
|
||||
{#if tweet.likes}
|
||||
<div
|
||||
in:fly={{ y: 40 }}
|
||||
out:fly={{ y: 40 }}
|
||||
>
|
||||
{tweet.likes}
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a
|
||||
href="/home/profile/{tweet.name}/status/{tweet.url}"
|
||||
class="permalink"
|
||||
title="Permalink"
|
||||
>
|
||||
<div class="circle">
|
||||
<Icon width="24" height="24" name="permalink" />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<form action="/home?_method=delete" method="post">
|
||||
<input type="hidden" name="id" value={tweet.id} />
|
||||
<button
|
||||
aria-label="Remove tweet"
|
||||
class="btn remove"
|
||||
title="Remove"
|
||||
type="submit"
|
||||
>
|
||||
<div class="circle">
|
||||
<Icon width="24" height="24" name="remove" />
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.avatar {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: var(--font-16);
|
||||
padding: var(--spacing-16);
|
||||
}
|
||||
|
||||
.tweet-container:hover {
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.tweet-container {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
gap: var(--spacing-16);
|
||||
padding: var(--spacing-16) var(--spacing-24);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tweet-container:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.tweet-details {
|
||||
display: grid;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.user {
|
||||
font-weight: 700;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.user:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.handle,
|
||||
.posted {
|
||||
font-size: var(--font-16);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: var(--font-16);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-32);
|
||||
margin-top: var(--spacing-16);
|
||||
}
|
||||
|
||||
.actions button,
|
||||
.actions a {
|
||||
padding: 0;
|
||||
color: var(--color-text-muted);
|
||||
background: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: var(--spacing-16);
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.circle > :global(.liked) {
|
||||
color: hsl(9 100% 64%);
|
||||
fill: hsl(9 100% 64%);
|
||||
}
|
||||
|
||||
.like {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.like:hover {
|
||||
color: hsl(9 100% 64%);
|
||||
}
|
||||
|
||||
.like:hover .circle {
|
||||
background: hsla(9 100% 64% / 10%);
|
||||
}
|
||||
|
||||
.permalink:hover {
|
||||
color: hsl(120 100% 40%);
|
||||
}
|
||||
|
||||
.permalink:hover .circle {
|
||||
background-color: hsla(120 100% 50% / 4%);
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
color: hsl(0 100% 50%);
|
||||
}
|
||||
|
||||
.remove:hover .circle {
|
||||
background-color: hsla(0 100% 50% / 4%);
|
||||
}
|
||||
|
||||
.like,
|
||||
.remove,
|
||||
.permalink {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.count {
|
||||
margin-left: var(--spacing-16);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
24
src/lib/date.ts
Normal file
24
src/lib/date.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export function timePosted(createdAt: Date): string {
|
||||
try {
|
||||
const posted = new Date(createdAt).getTime()
|
||||
const currentTime = new Date().getTime()
|
||||
const difference = currentTime - posted
|
||||
const seconds = difference / 1000
|
||||
const minutes = seconds / 60
|
||||
const hours = minutes / 60
|
||||
|
||||
if (minutes <= 60) {
|
||||
return `${minutes.toFixed()}m`
|
||||
}
|
||||
|
||||
if (hours <= 24) {
|
||||
return `${hours.toFixed()}h`
|
||||
}
|
||||
|
||||
return Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'medium'
|
||||
}).format(posted)
|
||||
} catch (error) {
|
||||
throw new Error(`💩 Something went wrong: ${error}`)
|
||||
}
|
||||
}
|
||||
5
src/lib/prisma.ts
Normal file
5
src/lib/prisma.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export default prisma
|
||||
10
src/routes/api/index.svelte
Normal file
10
src/routes/api/index.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<script>
|
||||
export let item
|
||||
</script>
|
||||
|
||||
<h1>{item}</h1>
|
||||
|
||||
<form action="/api" method="post">
|
||||
<input type="text" name="item" />
|
||||
<button class="btn" type="submit">Submit</button>
|
||||
</form>
|
||||
27
src/routes/api/index.ts
Normal file
27
src/routes/api/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
let item = 'banana'
|
||||
|
||||
export function get() {
|
||||
return {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function post({ request }) {
|
||||
const form = await request.formData()
|
||||
const newItem = form.get('item')
|
||||
|
||||
item = newItem;
|
||||
|
||||
return {
|
||||
status: 303,
|
||||
headers: {
|
||||
location: '/api'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,20 @@
|
|||
<script lang="ts">
|
||||
import Tweet from '$root/components/tweet.svelte'
|
||||
import type { TweetType } from '$root/types'
|
||||
|
||||
export let tweets: TweetType[] = []
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Home</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Feed</h1>
|
||||
|
||||
{#each tweets as tweet (tweet.id)}
|
||||
<Tweet {tweet} />
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
position: sticky;
|
||||
|
|
|
|||
43
src/routes/home/index.ts
Normal file
43
src/routes/home/index.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { RequestHandler } from "@sveltejs/kit";
|
||||
import prisma from "$root/lib/prisma";
|
||||
import { timePosted } from "$root/lib/date";
|
||||
|
||||
export const get: RequestHandler = async () => {
|
||||
const data = await prisma.tweet.findMany({
|
||||
include: { user: true },
|
||||
orderBy: { posted: 'desc' }
|
||||
})
|
||||
|
||||
const liked = await prisma.liked.findMany({
|
||||
where: { userId: 1 },
|
||||
select: { tweetId: true }
|
||||
})
|
||||
|
||||
const likedTweets = Object.keys(liked).map(
|
||||
key => liked[key].tweetId
|
||||
)
|
||||
|
||||
const tweets = data.map(tweet => {
|
||||
return {
|
||||
id: tweet.id,
|
||||
content: tweet.content,
|
||||
likes: tweet.likes,
|
||||
posted: timePosted(tweet.posted),
|
||||
url: tweet.url,
|
||||
avatar: tweet.user.avatar,
|
||||
handle: tweet.user.handle,
|
||||
name: tweet.user.name,
|
||||
liked: likedTweets.includes(tweet.id)
|
||||
}
|
||||
})
|
||||
|
||||
if (!tweets) {
|
||||
return { status: 400 }
|
||||
}
|
||||
|
||||
return {
|
||||
headers: { 'Content-Type': 'application/json ' },
|
||||
status: 200,
|
||||
body: { tweets }
|
||||
}
|
||||
}
|
||||
11
src/types/index.ts
Normal file
11
src/types/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export type TweetType = {
|
||||
id: number
|
||||
content: string
|
||||
likes: number
|
||||
posted: string
|
||||
url: string
|
||||
avatar: string
|
||||
handle: string
|
||||
name: string
|
||||
liked: boolean
|
||||
}
|
||||
Loading…
Reference in a new issue