Создаем блог на Svelte. Часть 5: Многостраничный сайт, авторизация, редактирование статей

Создаем блог на Svelte. Часть 5: Многостраничный сайт, авторизация, редактирование статей

Продолжаем пилить собственный блог на базе SvelteKit. На этот раз займемся реализацией перехода по ссылкам, создадим базовую навигационную панель, подключим авторизацию на базе Supabase, а также реализуем функции редактирования и удаления статей, которые и так уже давно напрашиваются. 

Предыдущий пост: Создаем блога на Svelte. Часть 4: Деплой 

Настраиваем отдельные страницы для статей 

Кажется, в нашем блоге статьи отображаются достаточно удобно и менять ничего не надо, но это только в том случае, если мы смотрим на самые примитивные варианты постов, состоящие из небольшого количества символов. Если мы попытаемся залить в базу данных длинный текст, то увидим то, что изображено на скриншоте ниже. Он будет занимать слишком много пространства, поэтому для него нужно выделить отдельную страницу. 

Пример большого текста в интерфейсе блога

Мы, конечно же, не будем вручную для каждого поста создавать отдельную страницу, а воспользуемся возможностями роутинга, встроенными в SvelteKit. С помощью них можно создать специальный файл, способный трансформироваться, принимая внешние данные и отображая необходимую информацию в зависимости от этих внешних данных. Проще говоря, создадим универсальную страницу для всех наших статей. 

Для начала нужно создать новый файл в проекте. Это можно сделать прямо в редакторе кода. Назовем этот файл [slug].svelte (квадратные скобки обязательны). Так часто называют контент универсального характера. Это же понятие можно увидеть в документации к Svelte. Тем не менее вы можете выбрать любое другое название, главное, чтобы такой файл был в директории в единственном экземпляре, чтобы не возникло путаницы. 

Прежде чем переходить к редактированию универсального файла для статей, добавим ссылку на него в нашем компоненте Post, отвечающем за рендеринг каждого поста. Мы обрамим в тег весь блок и добавим внутрь ссылку формата href={./$title}.

SvelteKit автоматически поймет, что речь идет о файле [slug].svelte и будет доставлять туда нужный нам контент. В нашем случае – это переменная title, в которой хранится название статьи.

Измененный код в компоненте Post

Также заранее сократим максимальную длину статей, чтобы весь текст каждого поста не отображался на главной странице (с помощью функции substring(0, 300), примененной к переменной body) и поставим фиксированную высоту для заголовков, указав Tailwind-класс h-20 для переменной title.

Сокращенный вид статей на главной

Теперь переходим к нашему файлу [slug].svelte. Нужно настроить его так, чтобы он подтягивал из базы данных нужную нам статью и отображал весь контент, который в ней найдется. Разбираем код построчно.

Импорты в файл slug

  1. Сначала импортируем Supabase, чтобы обращаться к базе данных – import supabase from ‘$lib/db’
  2. Затем импортируем функцию onMount с помощью import { onMount } from ‘svelte’
  3. Таким же образом импортируем функцию page. Она нам понадобится для добычи информации о странице, которая передается извне: import { page } from ‘$app/stores’
  4. Тут же эту информацию используем, чтобы вытащить название статьи, которое мы передаем через ссылку в компоненте Post – const title = $page.params.slug. Поясню, что тут происходит. Я обращаюсь к хранилищу данных $page (оно встроено в SvelteKit) и там беру параметры страницы с соответствующим названием. В нашем случае – Slug.
  5. Также я создаю пустую переменную post, которая станет временным хранилищем для статьи – let post = ‘’

Следующий этап – добавление статей из базы данных на нашу универсальную страницу. 

Функция загрузки постов из базы данных

• Создаем функцию onMount, в которую сразу же помещаем еще одну асинхронную функцию, как мы делали в предыдущей статье – onMount( async () => )

• В тело этой функции прописываем запрос к базе данных в следующем формате: 

let { data, error } = await supabase.from(‘Posts’).select(‘*’).eq(‘title’, title)

​Здесь мы не просто делаем запрос к таблице Posts. Мы выбираем оттуда все элементы и используем директиву eq, чтобы сравнить полученные данные с title, который мы указали ранее. Это необходимо для фильтрации лишних статей и отображения только той, что имеет нужное нам название.

• И полученную информацию мы отправляем в переменную post, чтобы потом использовать ее для рендеринга контента на странице – post = data[0]. Мы указываем индекс, равный нулю, потому что Supabase всегда возвращает массив, даже если мы запросили всего один элемент. 

Теперь можно прописать на нашей Slug-странице еще два блока: post.title для отображения заголовка и post.body для отображения тела статьи.

Интерфейс отображения постов в развернутом виде

В целом, этого должно быть достаточно. Страница создана и показывает нужный контент. Но мы исправим пару мелочей. 

Во-первых, добавим директиву sveltekit:prefetch в список атрибутов , чтобы контент со страницы начинал загружаться еще при наведении курсора, тогда общая скорость загрузки возрастет и сайт будет казаться более плавным и шустрым. 

Директива prefetch

А еще мы стилизуем наш блок с контентом, чтобы он выглядел так же, как посты на главной странице, но в расширенном виде:

Стилизованный блок для статей

Здесь все. Страницы есть. Они работают. Переходим к следующему этапу. 

Настраиваем авторизацию 

Регистрация требуется почти на любом сайте и наш блог не исключение. Да, пока смысла в этом мало, но чуть позже мы сделаем ее обязательной и добавим необходимость в регистрации для создания новых постов. Ну и необходимость входа для их редактирования или удаления. А перед реализацией интерфейса для создания учетной записи сделаем простенькую панель навигации.

Панель навигации

Нам нужен хоть какой-то способ навигации, чтобы можно было легко перемещаться между страницами отдельных статей, между страницей для регистрации и общим списком постов. 

Сделаем максимально простой вариант. Создадим div с двумя кнопками: «Статьи» и «Логин». 

Элементы авторизации в навигационной панели

Оба элемента будут ссылками:

• Одна ведет на страницу – href=“./” (Это, собственно, главная).

• А вторая на – href=“./login” (такого пути еще нет, но мы создадим его в ближайшее время).

Этот код можно написать в отдельном файле, но я рекомендую написать его в layout, чтобы наша панель навигации была видна на каждой странице сайта.

Компоненты для авторизации

Переходим к интересной части. Создадим интерфейс для логина и регистрации. 

Пока не начали анализировать код, создайте в директории routes папку loginComponent, а внутри два файла: SignUp.svelte и SignIn.svelte. Это и будут наши компоненты, вместе формирующие интерфейс для входа и регистрации на сайте.

Начнем с регистрации. Открываем файл SignUp.svelte и создаем блок .

Блок script в интерфейсе для авторизации

• Внутрь сразу добавляем импорт функции для управления базой данных – import supabase from ‘$lib/db’

• Затем создаем несколько переменных без присвоения им каких-либо значений. Это будут переменные email, password и alert. Думаю, вы и так понимаете, зачем нужны первые две. Третья в ближайшее время не понадобится.

Метод регистрации в блоге

Теперь напишем функцию для создания новой учетной записи в вашей базе данных. Для этого нужно прописать код, указанный выше. Разбираем его:

  1. Создаем асинхронную функцию с названием signUp – const signUp = async ( ) => 

  2. В тело функции записываем еще одну функцию try, чтобы подготовить обработчик ошибок. 

  3. В тело функции try записываем обращение к базе данных – let { user, error } = await supabase.auth.signUp( {email: email, password: password }). email и password повторяются дважды, потому что в первый раз это ключ объекта от Supabase, а второй – наша переменная с таким же названием. 

  4. В конце добавляем функцию catch(err) и в ее теле возвращаем err на случай, если что-то пойдет не так. 

Функция готова. Осталось сделать интерфейс для управления ею. 

Интерфейс для регистрации в блоге

Чтобы не тратить время и не изобретать велосипед, возьмем уже существующую форму и слегка ее подредактируем. 

• В событии onSubmit функцию addPost заменим на только что созданную функцию signUp.

• Первый лейбл подменяем на email

• Переменную внутри input (та, что bind:value) заменим на email, и выставим для нее такой же тип данных, чтобы форма не пропускала невалидные адреса (без значка @ и без корректного домена). 

• Второй лейбл заменяем на «Пароль».

• Блок textarea заменяем на input с типом password (чтобы в поле отображались звездочки вместо символов), а переменную меняем на password.

• Ну и заменяем текст для кнопки на «Зарегистрироваться». 

Также я слегка подкорректировал стили, но это не так обязательно. Я все еще предполагаю, что вы выбрали собственный стиль и следуете своим правилам оформления блога. 

Данную процедуру проделываем с файлом SignIn.svelte, отвечающим за вход на сайт с уже существующей учетной записью. 

Функция входа на сайт

Здесь мы снова занимаемся копированием кода. В будущем мы этот момент исправим и сделаем код более переиспользуемым.

Вставляем те же импорты и переменные, что использовали в SignUp.svelte.

import supabase from ‘$lib/db’

let email

let password

let alert

Нам нужны идентичные элементы, так как процесс входа требует от пользователя аналогичные данные. 

Также добавляем функцию для логина:

const signIn = async ( ) => 
try {
  let {user, error} = await supabase.auth.signIn(
  email: email, password: password})
} catch(err) {
    alert(err)
  }
}

Аналогичная логика сохраняется в визуальной репрезентации компонента. Заменяем функцию при событии onSubmit и меняем текст в кнопке на «Войти». 

Все, второй компонент тоже готов.

route для логина

Мы ссылаемся на файл login выше, но пока его не создали. Он объединит два компонента для регистрации и входа, а также даст возможность создать отдельную страницу для взаимодействия с назваными компонентными. 

Перед началом создадим новый файл с названием login.svelte в корневой директории проекта. Название пишем с маленькой буквы, потому что это не компонент, а отдельный путь (или роут), по которому можно будет перейти, как по ссылке. Проще говоря, мы делаем не блок, из которого конструируются сайты, а отдельную страницу, как и в случае со [slug].svelte.

Итак, нужный файл создан. Теперь пишем код.

Содержимое роута для управления блоками авторизации

• Для начала импортируем два компонента для регистрации и входа, что были созданы ранее:

import SignUp from ‘./loginComponent/SignUp.svelte’

import SignUp from ‘./loginComponent/SignUp.svelte’

• Затем создаем переменную loginMenu. От ее состояния будет зависеть, какой блок будет активен – регистрационный или блок для входа: let loginMenu = ‘signIn’

• Также создаем две функции, меняющие значения loginMenu на signIn или signUp

На этом логика роута почти заканчивается. Можно переходить к верстке. 

Создаем блок с двумя кнопками:

Интерфейс для компонента login

С функцией openSignIn, которая будет отображать блок для входа, и функцией openSignUp.

Также мы создадим блок #if. Так мы сможем настроить ситуационный рендеринг блока, чтобы в один момент времени на странице отображался только нужный контент. В нашем случае отображаемый контент будет зависеть от значения переменной loginMenu. Если оно равняется ‘signIn’, то будет отображаться соотвествующий блок. Если нет – будет отображаться другой. Конструкция у такого блока простая: пишем #if, внутри указываем нужный контент, а затем закрываем блок при помощи /if

Готово. У нас есть компонент, отвечающий за авторизацию. На скриншоте ниже видно, каким он получился у меня. У вас должно выйти что-то подобное. 

Пример оформления блока для авторизации

Наверху виднеется наша скромная навигационная панель

Реакция на успешный вход или регистрацию

Наша форма работает, но есть проблема. Если вы зарегистрируетесь в получившемся блоге, то этот процесс пройдет для вас незаметно. Вы останетесь на той же странице с заполненными полями. А отсутствие реакции от страницы – это ненормальное для нее поведение. Пользователь будет ждать хоть каких-то действий – переадресации на нужный раздел, в худшем случае – хотя бы уведомление. Так что этот момент тоже нужно отработать. Мы сделаем переадресацию. 

Начнем, как и в предыдущий раз, с регистрации. 

Нам нужно решить, куда будем направлять пользователя после успешного завершения регистрации. Я предлагаю сделать отдельную страницу под названием success.svelte и отправлять человека туда. А на странице его встретит уведомление о случившемся и предложение вернуться на главную страницу. Идеальный вариант. 

Создаем файл success.svelte в директории loginComponent (мы создали ее, когда делали компоненты для регистрации и входа). 

Реакция на успешную регистрацию

Внутри файла создаем небольшой блок, текст в духе «Вы успешно завершили регистрацию» и ссылку на главную страницу. Ничего нового, мы не просто так в начале делали эти ссылки и переходы. 

Теперь переход на страницу success.svelte надо автоматизировать, чтобы посетителю сайта не приходилось совершать какие-то лишние действия после ввода почтового адреса и пароля. Переходим в компонент SignUp.svelte и дополняем его.

Импорт функции goto

Добавляем строку import { goto } from ‘$app/navigation’. Так мы импортируем в приложение функцию goto, помогающую активировать переход на другие роуты без использования html и атрибута .

Переадресация после успешной регистрации

Затем надо отредактировать саму функцию signUp. Добавим в конец ее вызов функцию goto с аргументом в виде пути до файла success.svelte. Итоговый результат будет выглядеть так: 

goto(‘./loginComponent/success’)

С переадресацией в случае успешного логина все еще проще. Просто импортируем функцию goto в компонент SignIn.svelte и с помощью нее отправляем посетителя на главную. Получится goto(‘/‘)

Остался один нюанс, касающийся работы Supabase. Нам нужно отключить проверку по электронной почте, чтобы проверить работу сайта на выдуманных почтовых адресах. Для этого:

• Открываем настройки базы данных. 

• Переходим в раздел Authentication.

• Выбираем пункт Settings.

• Переключаем тумблер напротив строки Enable email confirmation.

Настройки Supabase

Настраиваем возможность редактировать и удалять статьи

Заключительный пункт в нашем плане на сегодня – редактирование постов. То, что стоило бы сделать раньше, но руки дошли только сейчас. 

Редактирование

Есть вероятность, что уже опубликованные посты потребуется отредактировать. Эту возможность довольно легко обеспечить, используя редактор для создания новых статей. 

Мы слегка модифицируем компоненты, отвечающие за отображение каждой отдельно взятой статьи. Речь идет о файле [slug].svelte. Открываем его и приступаем к изменениям. 

Сразу добавляем несколько новых переменных, в которых будет храниться измененный текст и название поста. У них те же имена, что используются в компоненте NewPost.svelte при создании новых записей, то есть postName и postBody. Также добавляем переменную inputShown. Она нужна, чтобы контролировать отображение редактора. 

Выведем в отдельную функцию запрос к базе данных. Нам это нужно, чтобы не писать несколько раз одну и ту же логику и иметь возможность получать актуальную информацию о статьях. Также мы добавим туда две новые функции: postName = post.title и postBody = post.body. Так мы заменяем контент в редакторе статьи. 

Обновленная функция getData

Потом получившуюся функцию getData закидываем в onMount, чтобы она продолжала автоматически запускаться при посещении страницы. 

Теперь мы создадим функцию editPost. В ней будет заложена вся логика обновления информации о статье и переадресации пользователя. Разберем весь код построчно:

Функция editPost

• Создаем асинхронную функцию const editPost = async ( ) => 

• В тело этой функции добавляем запрос на обновление информации в базе данных. Выглядит это и работает примерно так же, как и создание новых статей. Синтаксис функции следующий: 

let { data, error } = await supabase.from(‘Posts’).update({title: postName.replace(/[ ]$/, ‘’)}, body: postBody ).eq(‘title’, title)

В этой функции мы запрашиваем доступ ко всем постам, затем активируем метод update, чтобы обновить заданные переменные в БД. А потом указываем, что обновить нужно только строки, совпадающие по названию с той, которую мы редактируем. Вы могли заметить, что вместо postName используется результат работы функции replace. Это необходимо, потому что иногда Supabase неадекватно реагирует на пробелы в конце строк, поэтому мы удаляем их с помощью регулярных выражений. 

• Далее пишем inputShown = false. Эта строка понадобится для скрытия редактора по окончании внесения изменений. 

• Меняем title на postBody.

• Запрашиваем данные для нового title, чтобы обновить информацию о статье в приложении – getData(title).

• И в конце перебрасываем пользователя на ту же страницу, чтобы заново запросить информацию о посте и показать актуальные данные без необходимости обновлять состояние сайта: goto(./${title}) 

Логика готова. Теперь нужно сделать интерфейс для редактирования.

Блок отображения материала

Для начала укажем свойство #if (post && !inpurShown), чтобы интерфейс отображался только тогда, когда выключен редактор текста. Затем добавляем кнопку Edit, чтобы по ее нажатию статья пряталась, а редактор открывался. 

Блок редактора статей

Сам редактор может выглядеть так же, как и тот, что используется при создании статей. Можете скопировать ту же форму и просто заменить в ней переменные, а функцию addPost поменять на editPost

Готово. Теперь можно отредактировать пост, не переписывая материал с нуля и не копируя информацию. 

Удаление

Последняя функция в этой статьей – удаление. 

Реализация похожа на реализацию редактирования и во многом с ней связана. Редактировать мы будем тот же файл [slug].svelte. И в этот раз начнем с интерфейса. Нам нужно сделать крохотную мелочь – добавить кнопку «Удалить статью» и привязать к ней функцию removePost, которую мы создадим через мгновение. 

Функция removePost

Теперь напишем функцию removePost. По своей структуре она идентична editPost, но вместо update используется delete. Также сразу после запроса к базе и удаления из нее строки мы делаем переадресацию. И тут немного интереснее, потому что мы будем перебрасывать пользователя в новый файл – [success].svelte. Мы его адаптируем для переиспользования сразу в двух местах. 

Скопируем в этот файл содержимое success.svelte, добавим переменную let message = $page.params. Импортируем функцию page с помощью import { page } from ‘$app/stores’. Затем вместо текста «Вы успешно зарегистрировались» подставляем переменную { message }.

Возвращаемся к removePost и добавляем в конце goto(‘/loginComponent/Статья удалена’). Таким образом, мы отправим пользователя на страницу [success.svelte] и в качестве параметра передадим сообщение об успешном удалении статьи. 

Отредактированная функция регистрации

Останется лишь то же самое сделать в функции signUp, чтобы она ссылалась на [success].svelte и передавала параметром сообщение об успешной регистрации. 

Привязываем возможность редактировать и удалять посты к их владельцам

Все наши функции работают, но есть большая проблема – наши посты может редактировать любой прохожий. То есть ваш пост может отредактировать или удалить человек, даже не зарегистрированный на сайте. Поэтому мы ограничим возможность даже видеть кнопку Edit

Для этого создадим хранилище данных. 

• Открываем директорию lib.

• Создаем в ней новый файл stores.js.

• Внутри этого файла импортируем объект Store, а затем создаем переменную user: 

Содержимое stores.js

После этого надо отредактировать функцию signUp. Добавим туда информацию о пользователе в переменную userInfo, заменив user в запросе к базе данных на user: userInfo, а потом перенесем ее в хранилище user при помощи команды $user = userInfo. Теперь при регистрации все данные пользователя будут попадать в хранилище. 

Это можно проверить. Импортируем в файле index.svelte наше хранилище user – import { user } from ‘$lib/stores’

Импорт хранилища

И используем его для приветствия. На скриншотах ниже демонстрируется работа этой функции. Мы входим в учетную запись, и на экране главной страницы отображается сообщение «Welcome». 

Как это должно выглядеть: 

Приветствие на главной странице блога

Но если обновить страницу, то информация из хранилища пропадает. Поэтому все привилегии (еще не настроенные) и сообщение с приветствием перестают работать, как должны. 

Чтобы это исправить, надо снова отредактировать содержимое stores.js. Импортируем туда Supabase – import supabase from ‘$lib/db’, далее дополним содержимое метода writable таким кодом – writable(supabase.auth.user() || false). Теперь информация о пользователе будет подгружаться в хранилище автоматически при каждом вводе логина и при перезагрузке страницы.

Модифицированная версия хранилища

Переходим к реализации ограничений. Нам нужно связать статьи с их владельцами. Для этого на этапе добавления статьи мы будем фиксировать уникальный идентификационный код пользователя и прикреплять его к статьям в базе данных. Добавим соответствующую логику в функцию addPost.

Сначала импортируем туда хранилище – import { user } from ‘$lib/stores’

Потом в список параметров статьи добавляем еще один – userId. Он должен быть равен $user.id. Так у нас получится установить взаимосвязь между элементами в блоге и их хозяевами. 

Обновленная функция добавления постов

Работает ли наша проверка, можно легко понять, если отредактировать файл [slug].svelte. В начале снова импортируем хранилище user. Потом устанавливаем ситуативный рендеринг, чтобы кнопка Edit отображалась только для пользователя, которому «принадлежит» статья.

Выглядит это так: #if ($user.id == post.userId). Если id пользователя совпадает с userId статьи, то кнопка отобразиться, а человек сможет редактировать или менять пост. 

Таким же путем мы можем добавить ограничение на кнопку создания новых постов только для зарегистрированных авторов. 

Вместо заключения

На этот раз инструкция вышла объемной. Тема обширная, но зато мы закрыли почти все аспекты по ней. Можно сделать код эффективнее, убрать мусор, перенести данные об авторизации в cookie и т.п., но пока обойдемся и этим. Если есть вопросы, пишите в комментариях, постараюсь помочь!

Ах, да. И не забывайте про коммиты. 

источник

Related Posts
AllEscortAllEscort